前言
非同步處理一直以來都是 JavaScript 開發者很常遇到的情境,在之前,我們很常使用 callback 去完成任務,當結構變得複雜時,容易形成所謂的 callback hell,造成程式碼難以維護;在 ES6 版本中,新增了 Promise 物件,它能夠將非同步流程包裝成簡潔的結構,並提供統一的錯誤處理機制,解決了傳統 callback hell 的問題。此篇將會解析 Promise 的處理流程與相關方法。
筆記重點
- 何謂 Callback 與 Callback hell ?
- Promise 物件建立與基本使用
- Promise 執行流程與錯誤處理
- Promise 靜態方法
- Promise 變體方法
- 改寫 Callback 函式
何謂 Callback 與 Callback hell ?
Callback 是 JavaScript 很常使用的一種處理方式,以下是一個簡單的 callback 範例:
1 | function alertMsg() { |
在一般開發中,我們很常需要使用非同步處理去完成任務,像是 XMLHttpRequest
、setTimeout
…之類的,以下使用 setTimeout
來模擬非同步事件:
1 | function alertMsg() { |
在 JavaScript 中,有 event queue
的特性,所有非同步事件都不會立即執行當下行為,而是將這些行為放到 event queue
中,等待事件觸發後再回來執行;從上面範例可得知,結果的順序改變了,如果我們要確保 main end
在最後觸發,只需將 main end
包裝成 function 並且當成 callback 傳進去就好了,如下範例:
1 | function alertMsg(callback2) { |
使用 callback 解決了非同步事件引發的問題,但當結構變得複雜時,使用 callback 容易產生 callback hell,使得後期維護非常痛苦,閱讀性也變得非常差,如下範例:
1 | const async_api1 = (callback2) => { |
當你有許多的行為需要按照順序執行下去,此時你的程式碼就會變得非常混亂,如下:
1 | async_api1(() => { |
讓我們來看看 ES6 新推出的 Promise 物件是如何改善 callback hell 問題的。
Promise 物件建立與基本使用
Promise 物件的建立
一個簡單的 Promise 語法結構如下:
1 | const async_api = () => |
我們先來看 Promise 的建構函式,它的語法如下:
1 | new Promise(function(resolve, reject) { ... }); |
用箭頭函式簡化一下:
1 | new Promise((resolve, reject) => { ... }); |
建構函式的傳入參數需要一個函式,參數名稱可自由定義,但建議要符合使用上的命名,如果沒有其他需求,使用 reslove 與 reject 更能夠提高其閱讀性。
Promise 基本的使用
在 Promise 中,我們可以使用 then()
與 catch()
來接收回傳的內容,接續 Promise 物件的建立中的範例:
1 | async_api() |
可能會有人看到的是下面這種寫法:
1 | async_api().then( |
事實上,兩者的結果是一樣的,then
方法接受兩個函式當作傳入參數,第一個函式為 Promise 物件狀態轉變成 fulfillment
所呼叫,有一個傳入參數值可用;第二個函式為 Promise 物件狀態改變成 rejected
所呼叫,也有一個傳入參數值可用。
為什麼說它是一樣的結果呢?對於 catch
方法來說,相當於 then(undefined, rejection)
,也就是 then
方法的第一個函式傳入參數沒有給定值的情況下,它算是個 then
方法的語法糖,這也代表著兩者在名稱的定義上有點不同,但意義其實是相近的。如果有 Promise Chain 的需求,盡量還是使用 catch
取代 then
的第二個函式,不然說實在的,對於結構性來講,會顯得非常混亂,下面是 Promise Chain
的例子:
1 | async_api() |
在 then
方法中的 fulfillment
函式,它是一個連鎖的結構,這也就代表著我們可以使用 return
語句來回傳值,這個值可以繼續往下面的 then
方法傳送,傳送過去的是一個新的 Promise 物件;而 rejected
這一個函式,也有連鎖結構的特性,但由於它是使用在錯誤處理情況,除非你要用來修正錯誤之類的操作,不然這樣子的回傳操作,可能會造成結構異常混亂,這也是我上面提到的問題。
Promise 執行流程與錯誤處理
throw 與 reject
在 Promise 建構函式中,直接使用 throw 語句相當於 reject 方法的作用,範例如下:
1 | const async_api = () => |
我們知道 throw
主要用來使 JavaScript 停止執行並拋出錯誤,在 Promise 中按照規則,只要有丟出例外動作,當下狀態就會直接變成 rejected
,這也是使用 throw
能夠達到與 reject()
同樣效果的原因,但這僅限於同步的操作,我們以下面範例做補充:
1 | const async_api = () => |
上面這是一個使用 setTimeout
模擬非同步事件的範例,對於 throw
在一般程式使用中,會拋出錯誤並停止執行,在 Promise 的同步操作中,Promise 會隱藏錯誤並將當下狀態更改為 rejected
,而在非同步操作中是無法隱藏錯誤的,這也代表 Promise 後續的連鎖都將出現錯誤,所以還是乖乖的使用 reject
方法就好,這才是正規操作 Promise 狀態的方法。
執行流程與錯誤處理
在前面我們有介紹到關於 Promise Chain 的相關操作,這次我們來探討關於執行流程與錯誤處理的相關內容,先來看一下範例:
1 | const async_api = () => |
在 then
的第一個回傳條件下,會尋找相符合的第一個狀態,如同上面範例會觸發第三個 catch
,接下來的回傳值都必須遵循下面的規範:
- 回傳值不是函式或物件,直接將回傳值狀態用
fulfilled
實現,例:String、Number。 - 回傳值是 Promise 物件,回傳 Promise 最後的操作結果,例:resolve 方法、reject 方法。
- 回傳值是函式或物件,判斷是否為包裝的 Promise 物件,如果是,回傳 Promise 操作結果,如果不是,則直接將回傳值狀態用
fulfilled
實現,例:Object
理解了上面三個規範,就能夠清楚的了解 then
的處理方式,如同上面這一個範例,雖然觸發了第三個 catch
,但回傳值卻是 Number
,這也就導致回傳值狀態用 fulfilled
實現,才會觸發第四個的 fulfilled 結果。
Promise 靜態方法
Promise.resolve
與 Promise.reject
是 Promise 的靜態方法,Promise.resolve
可直接產生 fulfilled
狀態的 Promise 物件,Promise.reject
則是 rejected
狀態的 Promise 物件,如下範例:
1 | let promiseObject = Promise.resolve(10); |
Promise.resolve
與 Promise.reject
與使用 Promise 建構式的方式相比,在使用上有很大的不同,以下面的例子說明:
1 | // 方式一: Promise 建構式 |
在上面的三種方法中,方法三會直接 throw
出意外,造成程式中斷,與 Promise 建構式相比,Promise 靜態方法對於錯誤的處理方式是不同的,如果你的函式處理較為複雜時,你在回傳 Promise.reject
或 Promise.resolve
前發生意外,是完全無法控制的。
結論是 Promise.reject
或 Promise.resolve
只適用於單純的純入物件、值、或外部的 thenable 物件,如果你要把一整段程式碼或函式轉為 Promise 物件,不要使用這兩個靜態方法,要使用 Promise 建構式來產生物件才是正解。
Promise 變體方法
Promise.all
是並行運算使用的變體方法,它的語法如下:
1 | Promise.all([...]); |
Promise.all
方法會將陣列中的值並行運算,全部完成後才會接著下個 thne
方法,在執行時有以下幾種狀況:
- 陣列中的索引值與執行順序無關,大家起跑線都一樣
- 陣列中的值如果不是 Promise 物件,會自動使用
Promise.resolve
方法來轉換 - 執行過程時,陣列中只要有任何一個 Promise 物件發生例外錯誤,或是有 reject 狀態的物件,會立即回傳一個 rejected 狀態的 Promise 物件。
- 實現完成後,接下來的 then 方法會獲取到的值為陣列值
下面為 Promise.all
的範例:
1 | const a1 = Promise.resolve(3); |
Promise.race
也是一個變體方法,它的語法如下:
1 | Promise.race([...]); |
Promise.race
方法就如同 Promise.all
,一樣都是並行運算的變體,差別在於 Promise.all
指的是所有的 Promise 物件都要 resolve 完成才進行下一步,而 Promise.race
則是任何一個 Promise 物件 resolve 完成就進行下一步。用 race(競賽)這個字詞是在比喻就像賽跑一樣,只要有任何一個參賽者到達終點就結束了,當然它的回傳值也只會是那個優勝者而已。
Promise.race
的規則與 Promise.all
相同,只不過實現的話,下一步的 then
方法只會獲取最快實現的那個值,範例如下:
1 | const test = (name, timeout) => |
改寫 Callback 函式
在前面我們有強調使用 callback 容易造成 callback hell,這一次我們使用 Promise 來改寫 callback 的範例吧:
1 | const async_api = (name, timeout) => |
很明顯的,使用 Promise 大幅提高了結構的易讀性,且提供統一的流程管理,光是這幾點,就值得你花時間學習它了,Promise 的應用場景一定不只有這些,非同步對於 JavaScript 來說,幾乎是必備的條件,而 Promise 物件的出現,就是用來改善傳統非同步情境可能會發生的問題,好東西,不學嗎?