JavaScript 的 Callback, promise, async 及 await
在做 Web App 時不可避免的就是要執行「非同步」的動作,英文稱之為 Asyncrhonous action。
所謂的「非同步」指的是一個動作被分成兩個時期,初始化和執行。例如設定鬧鐘 10 秒後響鈴,初始化就是設定鬧鐘,執行就是當時間到之後響鈴。
我們來看看 JavaScript 中的非同步是怎麼被實做的。
Callback Function
JavaScript 中有所謂的一級函式(First-class Function),也就是可以把 Function 當變數傳來傳去,當作 Return Value 等等。
Callback Function 就是把一個 Function 當作變數傳入另一個 Function 中,在需要的時刻呼叫。
例如,設定鬧鐘 1 秒鐘後響鈴,我們可以先寫一個這樣的 Function 設定鬧鐘 1 秒後做某些事
1 | function setAlarm() { |
要做的「某些事」便可以寫成一個 Function 傳進去,例如印出 Ring!
,呼叫後這個 Callback Function 的內容便會在 1 秒鐘後被執行
1 | function callback() { |
成功與錯誤的 Callback Function
現實的場景常常要在 Callback Function 中 Handle Errors,因此要分成成功及失敗兩種情況。我們可以寫個 Function 模擬成功或失敗的情況,然後傳兩個 Callback Function 進去,一個處理成功,一個處理失敗
1 | function findEvenNumber(cbSuccess, cbFailed) { |
假設隨機產生 0 或 1 兩個數字,產生 0 就成功、1 就失敗,我們在 Callback Functions 裡印出成功或失敗
1 | findEvenNumber( |
執行後就能看到成功或失敗了。
Pyramid of Doom
我們接著可以做一件事,看看能不能連續找到 3 次偶數,可以的話就歡呼一下
1 | findEvenNumber( |
由於要第一次成功,我們才能在成功的 Callback 中再呼叫一個 Callback,第二次成功再呼叫裡面一層的 Callback,以此類推。
顯然會讓整段程式碼非常的「巢」,可讀性不佳。這樣巢狀的寫法寫到後面,中間變的很深的樣子也被稱為 Pyramid of Doom 金字塔毀滅。
我們可以做的是把這些 Callbacks 拆開寫
1 | findEvenNumber(findEvenNumberSuccess1, findEvenNumberFailed1); |
看起來可讀性就比較高一些了,但這樣子的做法就會將 Function 的連慣性拆開成一塊一塊的,那有沒有既可以保有連續性,又比較好的做法呢?
接下來要談的 Promise 就可以解決這個問題。
Promise
Promise
Object 是一個 JavaScript 內建的 Object,用來處理非同步動作的成功與失敗。
使用方式很簡單,新增一個物件,傳入一個 Callback Function,裡面兩個參數分別是成功和失敗的 Callback Function,就和我們上面寫的類似,只不過 JavaScript 已經有內建的 Class 幫你做而已
1 | const promise = new Promise(function (resolve, reject) { |
我們用 Promise 改寫找到偶數的 Function
1 | new Promise(function (resolve, reject) { |
再來要如何使用 Promise 呢?如果成功的話,意味我們在 Promise 裡面呼叫了 resolve()
,此時我們可以呼叫這個 Promise 的 then()
Function
1 | new Promise(function (resolve, reject) { |
跑完這段程式碼,有 1/2
的機率成功,會印出 Successful
。那失敗呢?在 Promise 的 Function 裡有個 catch 可以做到
1 | new Promise(function (resolve, reject) { |
Promise 怎麼做的?
那 Promise 究竟是怎麼做,讓這個 Promise 物件呼叫 then()
和 catch()
對應到 resolve()
及 reject()
的?
用狀態:一個 Promise 有 3 個狀態
pending
fulfilled
rejected
剛開始被初始化時,Promise 的狀態是 pending
,等到我們在 Function 中呼叫 resolve()
或 reject()
,Promise 就會被改變狀態成 fulfilled
或 rejected
。
還記得我們後面呼叫的 .then()
和 .catch()
嗎?裡面傳入的 Callback Function 會因為 Promise 的 State 轉變成 fulfilled
或 rejected
而被呼叫。
Promises Chaining
到目前為止,和單純的使用 Callback Function 來實做的差異不會太大,而 Promises Chaining 這個 Feature 就讓 Promise 的優點體現出來了。
我們再回頭看看,連續 3 次為偶數的程式碼,怎麼用 Promise 實現
1 | function findEvenNumber(count) { |
我們一樣新增一個 Function 叫做 findEvenNumber,但傳入一個試第幾次的變數 count
,並且回傳一個 Promise,裡面的 resolve()
和 reject()
也分別傳入 count
及一個 Error Object。
試著呼叫這個 Function 並傳入 resolve()
及 reject()
的 Callback Function
1 | findEvenNumber(1) |
可能出現的結果就會是
1 | Try count: 1 |
或
1 | Try count: 1 |
這樣是回傳一個 Promoise 出來,再用 .then()
去鍊起來的做法,就稱為 Promises Chaining。裡面的事當然可以隨便做,常見的像是送出一個 API Request,回來後再送另外一個,就可以這樣使用。
Async/await
Async/await 是一種 Syntax,能更為直覺使用 Promise。
例如
1 | async function plusCount(count) { |
如果沒有加這個 async
的 keyword 在宣告 Function 的前面,我們印出的值就會是 2
。但加上 async
後,印出來的東西就會一個 Promise 的物件!
也就說這種 Syntax 會幫你把裡面的回傳值變成一個 Promise 的物件。
還記得前面做的找到偶數這個 Function 吧?如果需要用到 Promises Chaining 來做多次的非同步操作,用 async
搭配 await
就會變的很簡潔。
只要在 async
宣告的 Function 裡面,我們就可以使用 await
這個 keyword
1 | function findEvenNumber(count) { |
得出的結果會和用 Promises Chaining 的版本一模一樣,但是做的事情卻像是處理 Synchronous 的 Function 一樣,呼叫 Function 後直接拿到回傳值、也可以用 try catch 來捕捉錯誤。
參考資料
- JavaScript.Info - Introduction: callbacks
- JavaScript.Info - Promise
- JavaScript.Info - Promises chaining
- MDN - First-class Function
- MDN - Promise