Callback Function & Promise
前言
在 2018 iT 邦幫忙鐵人賽,看到 Kuro 大的重新認識 JavaScript 系列文,仔細的閱讀後,紀錄自己觀念不足的部份,也非常推薦給大家觀看此系列文。
Callback Function
概念就如同:
辦公室電話響了 (事件被觸發 Event fired) -> 接電話 (處理事件 Event Handler)
而寫成程式碼就類似:
1 | // 註:這裡只是比喻,並沒有電話響這個事件 XD |
可以看到,Office 透過 addEventListener 方法註冊了一個事件,當這個事件被觸發時,它會去執行我們所指定的第二個參數,也就是某個「函式」(接電話)。
換句話說,這個函式只會在滿足了某個條件才會被動地去執行,我們就可以說這是一個 Callback function。
波動拳 (a.k.a. “Callback Hell”)
除了事件以外,還有另一個會需要用到 Callback function 的場景,就是「控制多個函式間執行的順序」。
下面舉例從簡單的事情慢慢演變成複雜時,會發生什麼情形
這裡定義了兩個 function:
1 | var funcA = function() { |
因為 funcA 與 funcB 都會立即執行,所以執行結果必定為:
1 | "function A"; |
但是,假設我們改成這樣,加上一個隨機生成的等待時間:
1 | var funcA = function() { |
這時候就沒辦法確定是 “function A” 會先出現還是 “function B” 會先出現了對吧?
像這種時候,為了確保執行的順序,就會透過 Callback function 的形式來處理:
1 | // 為了確保先執行 funcA 再執行 funcB |
像這樣,無論 funcA 在執行的時候要等多久, funcB 都會等到 console.log(‘function A’); 之後才執行。
不過需要注意的是,當函式之間的相依過深,callback 多層之後產生的「波動拳」維護起來就會很可怕!
1 | getData(function (a) { |
再見 Callback Hell
執行順序的問題是一個,還有另一個常見的狀況是這樣,再回到 「Overcooked」 的場景。
假設邊緣人如我,只能自己一人玩 Overcooked,在領完食材原料之後,一樣會有青菜、番茄需要處理。
因為只有一個廚師,所以要嘛先處理青菜、要嘛先處理番茄,必須先弄完一項之後再去處理另一項,整個流程會被前一個步驟卡住。
像這樣「先完成 A 才能做 B、C、D …」的運作方式我們就會把它稱作「同步」(Synchronous) 。
當我要確保「切青菜、切番茄、擺盤」三個動作都完成之後,我才能繼續「上菜」這個動作。
在面臨這種問題的時候,我要怎麼確保三個動作都完成之後,才繼續執行後面的程式呢?
最直覺的方式是新增一個變數來管理狀態:
1 | var result = []; //紀錄已完成的事件 |
像上面這樣,當我們依序執行了 funcA()、funcB()、funcC(),由於內部 setTimeout 會等待亂數時間的關係,我們無法得知誰先誰後。 但可以確定的是,當這三個函式執行的時候就會去檢查 result.length === step ,如果成立,就表示三個任務都已經完成,那麼就可以再去呼叫 funcD 執行後續的事情。
如果不希望使用全域變數來污染執行環境的話,甚至可以包裝成一個通用的函式:
1 | function serials(tasks, callback) { |
那麼改寫一下 funcA()、funcB()、funcC():
1 | function funcA(check) { |
最後呼叫的時候,我們就可以透過這樣呼叫 serials() :
1 | serials([funcA, funcB, funcC], funcD); |
把想要提前執行的函式以陣列的方式傳進 serials() 作為第一個參數,當陣列中的函式都執行完畢後,才會呼叫第二個參數的 funcD()。
Promise 物件
為了解決同步/非同步的問題,自從 ES6 開始新增了一個叫做 Promise 的特殊物件。
簡單來說,Promise 按字面上的翻譯就是「承諾、約定」之意,回傳的結果要嘛是「完成」,要嘛是「拒絕」。
實際寫成 Promise 的程式碼大概像這樣:
1 | const myFirstPromise = new Promise((resolve, reject) => { |
要提供一個函式 promise 功能,讓它回傳一個 promise 物件即可:
1 | function myAsyncFunction(url) { |
當 Promise 被完成的時候,我們就可以呼叫 resolve(),然後將取得的資料傳遞出去。 或是說想要拒絕 Promise , 那麼就呼叫 reject() 來拒絕。
一般來說, Promise 物件會有這幾種狀態:
- pending: 初始狀態,不是 fulfilled 或 rejected。
- fulfilled: 表示操作成功地完成。
- rejected: 表示操作失敗。
整個 Promise 流程可以用這張圖表示:
如果我們需要依序串連執行多個 promise 功能的話,可以透過 .then() 來做到。
以剛剛的 funcA()、funcB()、funcC() 來當範例,我們將這三個函式分別透過 Promise 包裝:
1 | function funcA() { |
最後透過呼叫
1 | funcA() |
就可以做到等 funcA() 被 「resolve」之後再執行 funcB(),然後 resolve 再執行 funcC() 的順序了。
如果我們不在乎 funcA()、funcB()、funcC() 誰先誰後,只關心這三個是否已經完成呢?
那就可以透過 Promise.all() 來做到:
1 | // funcA, funcB, funcC 的先後順序不重要 |