Matsu

Nothing more than curiosity

玩轉Typescript

Yesterday is gone. Tomorrow has not yet come. We have only today. Let us begin.
― Mother Theresa

五月的最後一天,明天開始將掀開六月的篇章,未來還有很多挑戰,但今天才是最重要的。

今天想透過React使用Typescript去建立一個簡單的Todolist清單,在四月初其實有使用過React實作過,但當時的自己對於React可以說是完全不了解,很多東西都是到處參考,相當雜亂。本篇將透過介紹Typescript開始玩轉待辦清單(Todolist)。

-預備知識-

  • useRef、useContext Hooks使用方式

  • Typescript待辦清單
    • 介紹Typescript
    • 實作Todolist

Typescript待辦清單

開始實作前需要先理解一下Typescript是什麼,乍看下,跟Javascript很相似。我們的直覺並沒有錯,Typescript確實就是Javascript的超集合,換句話說,有點像是C++與C的關係,C++整體而言多出OOP的概念。Typescript則是比Javascript多出更嚴謹、明確的類別定義。

介紹Typescript

個人而言,Typescript給我的感覺很像是C,不知道為什麼在寫Typescript的時候,腦中一直浮現C的感覺,說起來相當微妙,不過卻又不盡相同。我們看一下範例:

let num: number = 2021;

唯一不同於JS的地方在於變數名稱後方多了冒號:,冒號後方再接上變數型態。Typescript的名稱很明顯告訴我們,它的主要概念就在於定義Type,嚴謹的定義,可以免於許多不明確的程式碼。

變數可以定義,函數沒有道理不能定義:

function add(a: number, b: number): number| string { return a + b; }

參數的定義是number外,回傳值則是設定為number或string,不覺得很像是C語言嗎? 好吧,可能我深受C的荼毒有點深。特別去注意到,若我們沒有給參數任何型態,一般來說,IDE都會跳出警訊,如下:

function printEverything(value) { console.log(value); }

function printEverything(value: any) { console.log(value); }

此時就會跳出警訊,解決方式就是在value後方加上類別,若不想指定特定類別,至少也得加上any,TS才能理解我們並不想指定特定類別,才不至於產生錯誤。

重複定義其實會有點惱人,因為若需要更改物件的某些定義,就必須要逐一修改,因此TS有個更方便的定義類別方式type

Person
1
2
3
4
5
6
7
8
type Person = {
name: string;
age: number;
address: string;
};

let Matsu = Person;
let People = Person[];

往後若需要修改這個類別,僅需要修改Person,使用方式則和先前提到的number, string相似,若要使用類別作為陣列,僅需要在後方加上[]即可。

最後關於Typescript最特別的大概是所謂的通用型類別(Generic Type),實際看一下例子:

1
2
3
4
5
6
7
8
9
10
11
12
function insertValue<Type>(array: Type[], value: Type){
const newArray = [value, ...array];
return newArray;
}

const nums = [1, 2, 3];
const newNums = insertValue(nums, 4);

const strs = ['Matsu', 'Chen'];
const newStrs = insertValue(strs, 'Taichung');
// ↓
// const newStrs = insertValue<String>(strs, 'Taichung');

最特別的地方在於這段,這就是所謂的Generic Type,在此處會根據array和value的類別,去決定函數回傳的類別是數值或是字串,當然我們也可以明確地定義它們的類別,甚至是自行定義Generic Interfaces,不過超出本篇內容就先不提。

實作Todolist

首先要在React專案中使用Typescript,必須匯入相關的lib,幸運的是create-react-app同樣有指令可以幫助我們一塊兒處理這部分的問題:
npx create-react-app --template typescript
執行上述的程式碼後就可以正式使用Typescript於React當中。

首先建立Todolist項目的類別型態,單純用於明確定義資料的型別。因此在src資料夾下建立models的資料夾後在其中建立todo.ts的檔案,基本上,一個專案會有各種models去定義各自使用的類別,不過我們目前只需要todo項目的資料型別即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// In "todo.ts"
class Todo {
// 定義建構子(Contructor)使用到的變數類型
id: string;
name: string;

constructor(todoName: string) {
// 傳遞進來的值作為名稱指派給新實體 -> 新增新的項目時傳遞
this.name = todoName;
// 轉換日期作為辨識的id值
this.id = new Date().toISOString();
}
}

export default Todo;

完成建立後,TS與JS間的差別在於TS必須先行定義變數的類別,另外todoName乍看下還不太懂用意,它主要會在後續新增項目的函數內使用到。完成最重要的類別定義後,接下來我們使用ContextAPI管理使用的資料,而不是透過props去處理資料,相關優缺點可以參考先前的幾篇文章。

同樣地,src資料夾下新建立資料夾store存放Context,在store中建立todos-context.tsx的檔案,注意到先前是jsx這裡自然而然就要改寫成tsx才能夠編譯TS相關的程式碼,往後就不再贅述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// In "todos-context.tsx"
import React, { useState } from 'react';
import Todo from '../models/todo.ts';

// 定義通用類型
type TodoContextObj {
todos: Todo[];
addTodo: (name: string) => void;
deleteTodo: (id: string) => void;
}

// 建立Context API
export const TodoContext = React.createContext<TodoContextObj>({
todos: [],
addTodo: () => {},
deleteTodo: (id: string) => {}
});

// React.FC告訴TS這是一個React函數組件,需要有props可以傳遞
const TodoContextProvider: React.FC = (props) => {
// 剛開始初始化時為空陣列,後續更新過後則為Todo類別
const [todos, setTodos] = useState<Todo[]>([]);

// 新增項目
const addTodoHandler = (todoName: string) => {
const newTodo = new Todo(todoName);

// 透過先前的陣列狀況去更新陣列
setTodos((prevTodos) => {
return prevTodos.concat(newTodo);
});
}

// 刪除項目
const deleteTodoHandler = (todoId: string) => {

setTodos((prevTodos) => {
// 透過Array內建函數filter過濾掉id相符的項目
return prevTodos.filter( todo => todo.id !== todoId);
})
}

const todoContextValue: TodosContextObj = {
todos: todos,
addtodo: addTodoHandler,
deleteTodo: deleteTodoHandler
}

// 傳遞狀態
return (
<TodoContext.Provider value={todoContextValue}>
{props.children}
</TodoContext.Prodiver>
)
}

export default TodoContextProvider;

使用TS最大優點在於嚴謹明確,但相對的也必須負擔較多的責任,透過使用Generic的方式,將自定義類別TodosContextObj分別傳入contextAPI與todosContextObj也是為了解決這部份的問題。相對於為各自的props重複撰寫,我想透過contextAPI去處理已經省略相當多重複的程式區塊。

到目前為止,我們已經完成最重要的兩步驟,定義類別與ContextAPI,處理完這兩個檔案後,最後僅需要將資料運用在組件上就算正式完成。

pic


結語

這篇文章主要講解Typescript的基礎及結合React使用的方式,透過搭配React Hooks展現出更強大的應用能力。希望能夠將內容講解得更詳盡,因此會分上下兩篇文章,若有任何問題歡迎私訊。

我個人挺喜歡Typescript給我的感覺,使用上雖然有比較多地方需要注意,卻給我一種不一樣的美,程式碼變得更加純淨的感覺。我想這就是學習的路上有趣的地方,Typescript明明要求得更多卻更簡潔有力,這不是一件很厲害的事情嗎? 看起來限制更多卻更能夠發揮自己的想法,雖然我理解的還很少,但我覺得Typescript很值得去學習。

React with JEST

You cannot change what you are, only what you do.
― Philip Pullman, The Golden Compass

  • 實際測試
  • 結語

今天延續上篇JEST測驗概念與使用的內容,若還沒看的朋友們,可以點開下方連結讀過再來看本篇。

React系列-JEST測試概念與使用: https://tinyurl.com/yh5puf3t

前幾天稍微聊過關於測試的類別與JEST的使用,最後展示基本範例的測試過程。在本篇我們將來討論其他方面的測試。話不多說,現在就來進入正題。


實際測試

首先建立新的檔案叫做GoodBye.js:

1
2
3
4
5
6
// In GoodBye.js
const GoodBye = props => {
return <p>{props.children}</p>
};

export default GoodBye;

建立這個檔案用途是能夠在Greeting.test.js當中,一同測試GoodBye組件,去試著了解整合測試的概念。同時,我們在Greeting組件匯入GoodBye並且將檔案修改一下,相信大家對於props.children不會太陌生,用一句話來說的話,就是打包內容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// In "Greeting.js"
import GoodBye from './GoodBye';

const Greeting = () => {

return (
<div>
<h2>Hello World!</h2>
<GoodBye>It is good to see you.</GoodBye>
</div>
);
};

export default Greeting;

接著修改測試檔案的內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// In "Greeting.test.js"
describe('<Greeting />', () => {
test('Render Hello World as a text', () => {
render(<Greeting />);

const findText = screen.getByText('Hello World', { exact: false });
expect(findText).toBeInTheDocument();
});

test('GoodBye in Greeting', () => {
render(<Greeting />);

const findGoodBye = screen.getByText('It is good to see you.', { exact: true});
expect(findGoodBye).toBeInTheDocument();
})
});

特別的是describe函數主要用來告知Test要測試的是一個組合(Suite),每一個describe可以有數個test,使用方式和test函數相似。延續上篇文章的測試後,出現下方結果。

test

緊接著來討論關於事件的測試方式,將Greeting組件修改,增加一個狀態管理文字是否顯示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { useState } from 'react';

import GoodBye from './GoodBye';

const Greeting = () => {
const [text, setText] = useState(false);

const changeText = () => {
setText(true);
};

return (
<div>
<h2>Hello World!</h2>
{!text && <GoodBye>It is good to see you.</GoodBye>}
{text && <GoodBye>Not good to see you.</GoodBye>}
<button onClick={changeText}>Change Text!</button>
</div>
);
};

export default Greeting;

因為測試事件需要模擬使用者點擊的行為,必須先引入userEvent物件進行模擬。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// In "Greeting.test.js"
import userEvent from '@test-library/user-event

describe('<Greeting />', () => {
...同上述測試碼...

test('Check original text is invisible', () => {
render(<Greeting />);

const button = screen.getByRole('button');
userEvent.click(button);

const originalText = screen.queryByText('It is good to see you.');
expect(originalText).toBeNull();
});
});

新增模擬按鈕的測試後,此處只有一個按鈕,直接透過getByRole去找到唯一的一個按鈕。實際上選擇方式和JS處理DOM的概念一樣,接著透過userEvent物件的方式click去模擬按鈕的行為,最後搜尋是否有原先的文字。注意到queryByText作用和getByText一樣,差別在於query若找到目標會回傳相符的節點,若沒有找到目標則會回傳null。現在執行測試就會看到三個測試通過囉!

test2

最後我們來看看本日的最後一個測試,主要針對fetch API和資料庫互動的測試。我們來仔細思考一下若測試時也真的與資料庫互動,會產生哪些問題? 以下是可能發生的問題:

  • 資料庫流量擁擠
  • 影響資料庫資料
  • 測試成本暴增

當我們主要想測試是否可以連線成功時,其實是完全不需要真的跟資料庫有後續的互動,即便是真的需要驗證資料庫是否可以成功運作,我想也會有一個模擬的資料庫,而不是與實際使用的資料庫溝通。我們先新增一個新的組件Async。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// In "Async.js"
import { useEffect, useState } from 'react';

const Async = () => {
const [posts, setPosts] = useState([]);

useEffect(() => {
// 網路上公開測試用API
fetch('https://jsonplaceholder.typicode.com/posts')
.then((response) => response.json())
.then((data) => {
setPosts(data);
});
}, []);

return (
<div>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
};

export default Async;

使用fetch API需要處理非同步的問題,因為從資料庫傳來的資料需要時間傳遞。在Async組件當中使用到useEffect和useState兩個Hooks,用途可以參見先前的文章。另外建立Async.test.js的檔案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { render, screen } from '@testing-library/react';

import Asnyc from './Async';

describe('Asynchronous API Testing', () => {
test('Check request successes or not', async () => {
// 建立Mock(Spy)
window.fetch = jest.fn();
// 覆寫fecth API的動作,連線後回傳資料
window.fetch.mockResolvedValueOnce({
json: async () => [{ id: 'p1', title: 'First Post' }],
});

render(<Asnyc />);

// 等待fetch動作完成後檢查是否有listitem陣列
const listItem = await screen.findAllByRole('listitem');
expect(listItem).not.toHaveLength(0);
});
});

處理非同步的問題需要覆寫fetch API的方法,這裡必須呼叫window.fetch才能夠正確覆寫,單純只呼叫fetch API會直接使用這個函數而無法覆寫。接著透過jest.fn函數去模擬回傳資料,jest.fn會建立所謂的Mock,Mock常常又稱作間諜,因為它可以模仿函數的行為,最後則是透過mockResolvedValueOnce去模擬資料的運作。一連串過程中,可以測試是否連線成功,唯一的差別在於資料是由測試人員自行建立。最後確認回傳的list長度是否為0,就能確認模擬狀況。這裡特別要注意到find的方法和get與query都一樣,差別在於它會回傳Promise,因此會等到模擬fetch完成後才執行。

最後我們執行測試,出現下方的結果。

test3

Test Suites共用兩個,因為我們分別有Async和Greeting兩個測試,此外內部分別有1和3個測試,總共有四個測試項目。

-重點回顧-

  • Query回傳DOM節點或Null
  • Find回傳Promise
  • Mock透過jest.fn建立

結語

今天把上一篇文章後續的內容討論一下,順便在腦中重新複習一下JEST測試的過程,不過如何針對props去測試還沒有仔細想過,或許會是未來一個有趣的內容。最近花兩天時間讓大腦好好放鬆,看完Netflix上的救命倒數這部美劇,第一季挺不錯的,但第二季就好冗長,跳過的內容相當多。另外艾莉西亞才真的值得珍惜,茱莉亞趕快下去…看的有夠討厭,連恩頭殼壞去,編劇要不要這麼搞?

最後謝謝看到這裡的妳/你們。

React with JEST

The unexamined life is not worth living.
― Socrates

這幾天在思考值得研究的主題,但是腦中卻一絲想法都沒有,我想是因為不斷在思考眼前的煩惱,而沒有空出時間給大腦去自由翱翔。不過,今天剛好學習到一門挺有趣的課程也是我想特別討論的主題,在求職過程中,我發現不少資深的職缺都有這部分的能力要求,那就是 Test。

蘇格拉底說:沒有經過審視的生命不值得去體驗。我想沒有經過測試的產品,也不值得人們去使用,程式碼也是一樣的道理,仔細去思考,一個產品只要有瑕疵就要面臨下架或者回收的下場,程式碼又何嘗不是如此,我們立刻進入主題。

  • React 與 JEST
    • 何謂 JEST
    • 單元測試、整合測試、端對端測試
    • 實際測試
  • 結語

React 與 JEST

今天主要透過 Jest 去測試 React,因為最近比較熟悉 React 框架,覺得透過 React 來學習會比較快上手。一般來說,專案都需要引入第三方的函式庫才能夠去執行測試,不過幸運的是,當我們透過create-react-app指令建立專案時,測試函式庫都會一併安裝完成,在 package.json 檔案中,我們能夠發現下方這三個套件。
"@testing-library/jest-dom": "^5.11.6"
"@testing-library/react": "^11.2.2"
"@testing-library/user-event": "^12.5.0"
當然就算沒有也沒關係,手動安裝一下,應該也不成問題。


JEST

首先從 JEST 開始談起,JEST 其實是專門用來測試所有 Javascript 程式碼的測試函式庫,不僅限於 React,同樣可以用於 Vue, Angular, TypeScript, Node 等等…。

JEST 大致上有幾大特點:

  • Fast/Safe: 平行執行測試且確保測試碼擁有唯一的全域狀態
  • Code Coverage: 透過設定–coverage 旗標來設定測試範圍
  • Easy Mocking: 執行模擬測試範圍外的程式碼
單元測試、整合測試、端對端測試

在 Coding 的過程中除錯,其實就是一種測試,大致上可以將測試分為以下兩類:

  • 手動測試
  • 自動化測試

但是我們都知道,只透過手動的測試,必然會出現許多的盲點,因此才會有自動化測試,而自動化又可以根據不同的測試要點,分為以下三類:

  • 單元測試(Unit Test)
  • 整合測試(Integration Test)
  • 端對端測試(End-to-End Test)

單元測試: 分離測試獨立區塊、函數、組件
整合測試: 整合測試數個區塊、函數、組件
端對端測試: 專案測試使用者體驗

基本上,最常看到的是單元測試,在一個專案當中,往往會將各種情境都盡可能測試過,因此大部分情況下都會針對個別的小區塊做測試。因此,一個專案可能會有數百數千個單元測試,數百個整合測試與數個端對端測試,根據情況去使用不同的測試。

但是要如何在專案中直接執行測試? 這就需要仰賴 JEST 去執行我們的測試碼,除此之外,我們還需要模擬 React App 的工具,也就是 React Testing 函式庫,上述兩個工具都已經在先前提到過。我們實際來測試看看。


實際測試

首先我們在專案當中建立 components 的資料夾,接著在資料夾中建立 Greeting.js 這個組件,如下:

1
2
3
4
5
6
7
8
9
10
11
// In "Greeting.js"
const Greeting = () => {

return (
<div>
<h2>Hello World!</h2>
</div>
);
};

export default Greeting;

現在我們已經有 Greeting 組件可以測試,但是現在要如何撰寫測試檔案? 一般來說,我們會在同一個資料夾直接建立同樣檔名的測試檔案,這裡我們在 components 建立 Greeting.test.js 測試檔。

1
2
3
4
5
6
7
8
9
10
// In "Greeting.test.js"
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';

test('Render Hello World as a text', () => {
render(<Greeting />);

const findText = screen.getByText('Hello World', { exact: false });
expect(findText).toBeInTheDocument();
});

測試Greeting組件要將它匯入測試檔中,此外還必須匯入兩個函數,分別為renderscreen:
render: 模擬React渲染
screen: 模擬DOM的選取

接著透過JEST的test函數,告訴React這裡是要進行測試的檔案。test('', {}),參數一描述測試用途,參數二則是用來撰寫測試的程式碼,可以注意到我們在這裡透過render()去模擬React渲染,接著透過screen去尋找是否有Hello World的文字,exact代表是否需要完全相符,預設情況下是false。最後透過expect函數去回傳測試結果。

實際跑過npm test就會出現下方測試通過的結果:
test1

這就是最簡單單單的單元測試,希望想對快速了解JEST和單元測試的朋友們有點幫助。


結語

原本想說要一次將測試的文章打完,但才打沒一半就篇幅就已經有點多,因此打算後續再根據這篇文章撰寫整合測試以及處理API的問題。很高興今天自己終於能夠對Test有點基礎概念,希望未來有機會能夠將它派上用場。我想這只能算是初窺JEST的堂奧,但作為認識JEST的第一步,應該算是相當直覺的了。

謝謝看到最後的各位。

JS - Async / Await入門

Be who you are and say what you feel, because those who mind don't matter, and those who matter don't mind.
― Bernard M. Baruch

今天來說說關於JS的同步與非同步問題。JS本身屬於單執行緒的語言,因此指令會依照順序或者透過特殊語法來控制執行的順序,這就是要來討論的主題。

  • 瑪德蓮蛋糕
    • 同步與不同步(Synchronous/Asynchronous)
    • Callback
    • Promise
    • Async/Await
  • 結語

瑪德蓮蛋糕

透過製作瑪德蓮來學習同步與非同步的概念,我想應該會相當有趣。瑪德蓮蛋糕是我偶爾會在家裡做的一種法式糕點,有興趣的朋友們也不妨嘗試做做看!
首先,我們先來看看瑪德蓮的製作過程:

  • 準備食材(食譜)
    • 雞蛋*2
    • 牛奶20g
    • 蜂蜜30g
    • 細砂糖40g
    • 低筋麵粉100g + 泡打粉3匙
    • 無鹽奶油85g
  • 攪拌全蛋
  • 加入砂糖、牛奶
  • 加入過篩的低筋麵粉、泡打粉
  • 加熱無鹽奶油
  • 加入融化奶油攪拌
  • 裝入擠花袋冷藏6小時
  • 烤箱預熱15分鐘170度
  • 擠上模具烘烤15分鐘
  • 取出完成

大致上的製作過程如下,其中有許多步驟是需要一步一步去做,但是這裡就牽涉到不少的同步與非同步的概念。

同步與非同步

同步(Synchronous): 同時執行動作
非同步(Asynchronous): 等待完成後執行下一個動作

因為JS特性的關係,我們沒辦法一次執行多個動作,舉例來說: 攪拌全蛋時加熱無鹽奶油,這對於JS本身來說是不可能的。況且有些時候,同步執行不會帶來好處,只會帶來災難,舉例來說,我們如果先預熱烤箱,但是我們根本還沒將瑪德蓮裝入擠花袋並且冷藏6小時,預熱的動作就完全只是徒勞。由此可見,同步與非同步都必須視情況而定。

我們嘗試將完成製作蛋糕的步驟簡單寫成JS的程式碼試試看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const Madeleine = () => {
prepare_ingredient();
stir_egg();
add_sugar_milk();
add_flour();
melt_butter();
add_butter();
put_to_bag();
freeze();
pre_heated();
bake();
};

// 準備食材
const prepare_ingredient = () => {};
// 攪拌全蛋
const stir_egg = () => {};
// 加入糖、牛奶
const add_sugar_milk = () => {};
// 加入麵粉
const add_flour = () => {};
// 加熱奶油
const melt_butter = () => {};
// 加入奶油
const add_butter = () => {};
// 裝入擠花袋
const put_to_bag = () => {};
// 冷藏六小時
const freeze = () => {};
// 烤箱預熱
const pre_heated = () => {};
// 烘烤
const bake = () => {};

Madeleine();

我將一連串的動作都寫成函數,接著在最後Madeleine函數去模擬做蛋糕,可以看到我們確實依照食譜的順序去逐一執行動作,JS也會根據程式的先後順序去執行。可是這裡似乎會發生一點問題,問題在哪裡? 問題在於,每一個動作間並沒有彼此認識,舉例來說: 在準備完材料後要攪拌全蛋,但是有沒有可能材料還沒準備完,就開始攪拌全蛋,而導致沒有確認無鹽奶油準備到,而後續造成melet_butter函數執行失敗,這就是此處的問題點所在。

Callback

先來聊聊關於Callback,Callback是JS裡面有趣的函數使用方式。我們先來看一下MDN上面的解釋:
A callback function is a function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action.
簡單來說Callback可以理解成函數執行完後要執行的函數,以上述Madeleine內的步驟來說:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Madeleine = () => {
prepared_Ingredient((res) => {
if(res === 'Ingredient prepared.'){
stir_egg((res) => {
if(res === 'stir finished.'){
add_sugar_milk(...其他Callback);
}else{
console.log('Not stirred yet!');
}
});
} else {
console.log('Ingredient not prepared yet!');
}
})
};

Madeleine();

上述的prepared_gradient在執行完成後,才呼叫stir_egg()函數,此外也可以透過Callback去處理錯誤的情況,這樣子動作就能夠認識彼此,更能夠了解彼此的優先順序,相較於先前全權交給JS執行緒處理,是不是更令人安心了? 但是這時候就出現所謂的Callback地獄,雖然Callback很方便也很好使用,但是過度使用,就會造成難維護的窘境。

Promise

MDN的描述:
A Promise is an object representing the eventual completion or failure of an asynchronous operation.
簡單來說,Promise是表達非同步執行結果的一個物件,會根據執行成功或失敗給予開發者回饋。因此,只要回傳Promise物件,我們都能夠使用Promise物件擁有的then函數和catch函數。then函數可以擁有兩個Callback代表Promise執行成功與失敗後的動作,而catch函數只擁有Promise執行失敗後的Callback。直接更改上述烘焙的程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 假設Madeleine回傳Promise
const Madeleine = new Promise((resolve, reject) => {
//暫定每1s成功執行後執行下一個then()
setTimeout(() => {
resolve('Madeleine');
}, 1000);
});

Madeleine()
.then(prepare_ingredient(), {
console.log('Ingredient not prepared yet!');
})
.then(stir_egg(), {
console.log('Not Stirred yet!);
})
.then(add_sugar_milk(), {
console.log('No milk to be added!');
}
...其他的thenc函數
.catch(() => {
console.log('Failed to make Madeleine');
});

透過Promise的使用,我們可以更好的去控管整個製作的流程,也就是將Callback管理的更好。

這邊要特別注意到new Promise這一段程式碼,這裡其實就是將Madeleine指派一個Promise的物件,而new Promise這個物件通常會有兩個參數resolve和reject,前者代表成功後要執行的行為,後者代表失敗後要執行的動作,相似於then和catch的概念,因為then和catch就是透過它們包裝而來的呀!

Async/Await

最後要講的是JS改版至ES7後的大明星Async和Await,因為它們的出現,我們得以從Promise Chain的枷鎖中獲得解放。實際看一下如何透過Async/Await來改寫我們在Promise的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const Madeleine = async() => {
console.log('Start making Madeleine');
const ingredient = await prepared_ingredient();
const stirredEggs = await stir_egg(eggs);

...其餘動作

console.log(ingredient);
console.log(stirredEggs);
...其他結果
};

const prepared_ingredient = () => {
return new Promise((resolve, reject) => {
if(egg && milk && sugar && honey && butter && flour){
setTimeout(() => {
resolve('Ingredient is prepared.');
}, 3000);
} else {
reject('Ingredient is not prepared!');
}
});
}

const stir_egg = (eggs) => {
return new Promise((resolve, reject) => {
if(eggs === 2){
setTimeout(() => {
resolve('Eggs are stirred.');
}, 5000);
} else {
reject('No enough eggs!');
}
});
}

... 其他動作的函數

透過async的關鍵字,我們可以告知JS要將Madeleine當作非同步的函數,此外因為其他動作函數也會是非同步,可以確定在Madeleine內執行的動作都是await等待過後,才會接續執行,而不會發生動作還沒完成就執行下一個動作的問題。


結語

花了很多時間查看許多文章,對於Callback, Promise, Async/Await,釐清了許多問題,但是還是覺得文章打得不夠好,還不能講解的淺顯易懂,代表我自己還沒有真的很熟悉這些概念以及實作方式。期望接下來能夠對這三者有更深的見解,屆時會再來修正這篇文章。

最有生產力的一年(The Productivity Project) - Chris Bailey

約莫一周過去,偶爾翻開小讀一番,不知不覺在今天讀完整本。

我想,這本書會吸引一個人,最根本的原因是不滿足於現狀的生活和自己。在有數十本的購買清單中,挑選這本書作為自己最優先閱讀的一本書,是因為我想知道自己和他人究竟有什麼區別。我曾經想過他人花的時間比我多,固然能夠成就比我更多,但在我約莫數個月的密集學習後,我發現自己的學習成果相當差。或許這樣的想法過於主觀,但我還是認為自己付出的時間,卻沒有得到我認為應該要有的成效,不免有點失望。當然,這本書還沒有完全解決我的問題,但卻是一本認清自己的最佳書籍。

  • 本書觀點
  • 我的看法
  • 結語

本書觀點

斷斷續續地讀這本書,說真的有些內容我早已是拋諸腦後,一方面是因為自己記憶力不好,另外一方面則是因為沒有照著書本去實際改善,而只是一昧地閱讀。不過,趁著這篇文章,可以好好溫習一些不錯的想法,因為在閱讀時,我有習慣在不錯的觀點上貼上色紙的習慣。

若有一句話可以歸納「最有生產力的一年」這本書,我想可以用下面這句話試著詮釋:
邊緣系統與前額葉皮層間的愛恨情仇

我不確定這兩個專有名詞究竟出現多少次,但至少就我個人看完這本書,就算不特別去記,也已經深植我的腦內海馬迴。喔!就連這個掌管長期記憶的腦內單元都跑出來,想必是Chris Bailey惹的禍。

用簡單的方式理解這兩個專有名詞,我想前者可以聯想至「衝動」,後者可以聯想至「理性」,但我認為這兩個詞彙沒有正負的區分,並不是衝動就不好,也不是理性就好,這全仰賴於生活所遭遇到的情境。

克里斯在開頭不久說: 「生產力與你做多少無關,只與你成就多少有關。」這是一句相當發人省思的一句話,因為在當今社會體制下,許多人依舊活在時間經濟時代,而不是知識經濟時代。在約莫1960s,投入的時間夠多,就會有相對應的報酬,但是在現今的社會下,投入的時間愈多,依舊只會獲得一樣的報酬,而這報酬卻不足以支付快速動盪的世界格局。

影響一個人的生產力主要由「專注力」、「精力」、「時間」三者構成,其中作者闡述時間是最為有限的。透過研究精力、專注力,作者提供我們許多方法去提升自己的生產力,甚至避免在提升生產力過程中的陷阱,赫赫有名的規劃謬誤同樣也出現在本書當中。

作者歸納並且提供給讀者的方法,我認為都有他的用意所在,不管是冥想、重點清單、熱點或白日夢,都是不錯的嘗試方式,在平常鮮少有機會去認識自己的過程當中,實踐某幾項會是不錯的嘗試。


我的看法

作者說時間是最為有限的,其實我並不完全認同,因為我個人認為專注力和精力的時間都不會超過24H,甚至是遠小於24H。

對於我個人而言最大的收穫是自我對話,當然多喝水、運動和冥想我認為也都是非常不錯且看來相當理所當然的概念,不過自我對話卻是一件很值得去思考的事情。作者認為當一個人生產力愈高,自我對話的頻率也會愈高。

很慶幸的是,自我對話中的自我否定的言語居然會高達7成,發現這件事情不是因為悲觀的心情而是人的根性是最大的收穫,頓時這周以來的烏煙瘴氣瞬間少掉一半。

規劃謬誤也是一個我很喜歡的論點,它雖然看起來有害,但我卻認為它與待辦清單(Todolist)息息相關,因為代辦清單不就是一種規劃嗎? 只不過這樣的規劃通常比較短期,但這又因人而異,因為代辦清單完全可以分長中短去撰寫,但確實認知自己掉落這樣的陷阱是必要的。我就是身受其害的一名,往往都把要讀的書本規劃好,卻根本沒有實踐,而掉落自我安慰的陷阱當中,以為一切都會照著規畫走。

很多情況下,我也經常強迫自己一定要完成才能夠去處理其他的事情,但卻沒有仔細去思考需要令自己適當的放鬆。愈想提高生產力,愈該學習時常的放鬆,而不是強逼迫自己去做該做的事情。以自己在學程式過程,剛開始的時候都會覺得信心滿滿,但一遇到問題就會累積焦燥的心情,甚至看著時間一點一點流逝,就會開始思考自己現在所做的是否有任何意義,也許只是自己的自我滿足,這也是作者認為理解提高生產力背後的意義至關重要的一點。

微小改變往往可以成就更大的目標,但在這其中不可或缺的是將「未來」的自己和「現在」的自己連結在一起,唯有將未來和現在的自己連結,才有可能真正理解自己的所作所為,這是本書我最喜歡的一個觀點。

0%