異步 Callback(回調)
Promise 中的所有回調函式,都是異步執行的
我需要再次強調,並非所有的使用 callbacks(回調)函式的 API 都是異步執行的。在 JavaScript 中,除了 DOM 事件處理中的回調函式 9 成 9 都是異步執行的,語言內建 API 中使用的回調函式不一定是異步執行的,也有同步執行的例如Array.forEach
,要讓開發者自訂的 callbacks(回調)的執行轉變為異步,有以下幾種方式:
- 使用計時器(timer)函式:
setTimeout
,setInterval
- 特殊的函式:
nextTick
,setImmediate
- 執行 I/O: 監聽網路、資料庫查詢或讀寫外部資源
- 訂閱事件
註: 執行 I/O 的 API 通常會出現在伺服器端(Node.js),例如讀寫檔案、資料庫互動等等,這些 API 都會經過特別的設計。瀏覽器端只有少數幾個。
那麼像那些為尚未支援 ES6 Promise 瀏覽器打造的 polyfill(填充)函式庫,又是怎麼作的?
一方面的確是用上面說的這些特別的方式,尤其是計時器的 API,也有可能會使用新式瀏覽器中獨有的功能。有個專案asap,裡面很多研究出來的異步執行方式,後來延伸出一套知名的 Promise 外部函式庫 - Q。而在es6-promise專案中,也有個自己的asap.js檔案,它是來自另一個異步程式的工具函式庫RSVP.js。
在 Promise 結構中異步回調函式只是其中一個重要的參與分子,但 Promise 的重點並不只是在異步執行的回調函式,它可以把多個異步執行的函式,執行流程轉變為序列執行(一個接一個),或是並行執行(全部都要處理完再說),並且作更好的錯誤處理方式。也就是說,Promise 結構是一種異步執行的控制流程架構。
Deferred 物件
如果你有用過近幾年在 jQuery 中的 ajax 相關方法,其實你就有用過它裡面 Deferred(延期)的設計了。Deferred(延期)算是很早就被實作的一種技術,最知名的是由 jQuery 函式庫在 1.5 版本(2011 年左右)中實作的 Deferred 物件,用於註冊多個 callbacks(回調)進入 callbacks(回調)佇列,呼叫 callbacks(回調)佇列,以及在成功或失敗狀態轉接任何同步或異步函式,這個目的也就是 Deferred 物件,或是 Promise 物件實作出來的原因。
Deferred 的設計在 jQuery 中 API 豐富而且應用廣泛,尤其是在 ajax 相關方法中,因為它的語法易用而且功能強大,更是受到很多程式設計師的歡迎。jQuery 所設計的 Deferred 物件中其中有一個deferred.promise()
可以回傳 Promise 物件。歷經多次的改版,現在在 3.0 版本中的 jQuery.Deferred 物件,與現在的 Promises/A+與 ES6 Promises 標準,已經是相容的設計可以交互使用,你可以把它視為是 Promise 的超集或擴充版本。
Deferred(延期)的設計在不同函式庫中略有不同,例如 Q 函式庫(或 Angular 中的$q),它把 Promise 物件視為 Deferred 物件的一個屬性,在使用上兩者扮演不同的角色。Deferred(延期)並不在我們要討論的細節。不過,相對來說 ES6 中的 Promise 物件功能較少。
如果你有需要使用外部函式庫或框架中關於 Promise 的設計,以及使用它們裡面豐富的方法與樣式,建議你不妨先花點時間了解一下 ES6 中的 Promise 特性,畢竟這是內建的語言特性,對於一般的異步程式設計也許已經很足夠。
異步程式設計與 Promise
promise 物件的設計就是針對異步函式的執行結果所設計的,要不就是用一個回傳值來變成已實現狀態,要不就是用一個理由(錯誤)來變成已拒絕狀態
同步程序你應該很熟悉了,大部份你寫的程式碼都是同步的程序。一步一步(一行一行)接著執行。異步程序有一些你可能用過,setTimeout
、XMLHttpRequest
(AJAX)之類的 API,或是 DOM 事件的處理,在設計上就是異步的。
我們關心的是以函式(方法)的角度來看異步或同步,函式相當於包裹著要要一起來作某件事的程序語句,雖然函式內的這些程序有可能是同步的也有可能是呼叫到異步的其他函式。JavaScript 中的程式執行的設計是以函式為執行上下文(EC)的一個單位,也只有函式可以進入異步的執行流程之中。
同步執行函式的結果要不就是回傳一個值,要不然就是執行到一半發生例外,中斷目前的程式然後拋出例外。
異步的函式結果又會是什麼?要不然就最後回傳一個值,要不然就執行到一半發生例外,但是異步的函式發生錯誤時怎麼辦,可以馬上中斷程式然後拋出例外嗎?不行。那該怎麼作?只能用別的方式來處理。也就是說異步的函式,除了與同步函式執行方式不同,它們對於錯誤的處理方式也要用不同的方式。
異步執行函式的結果要不就是帶有回傳值的成功,要不就是帶有回傳理由的失敗。
以一個簡單的比喻來說,你開了一間冰店,可能有些原料是自己作的,但也有很多配料或食材是由別人生產的。同步函式就像你自己作配料的流程,例如自己製作大冰塊、煮紅豆湯之類的,每個步驟都是你自己監管品質,中間如果發生問題(例外),例如作大冰塊的冰箱壞了,你也可以第一時間知道,而且需要你自己處理,但作大冰塊這件事就會停擺,影響到後面的工作。異步函式是另一種作法,有些配料是向別的工廠叫貨,例如煉乳或黑糖漿,你可以先打電話請工廠進行生產,等差不多時間到了,這些工廠就會把貨送過來。當工廠發生問題時,你可能只是接獲工廠通知,你能作的後續處理有可能是要同意延期交貨或是改向別的工廠叫貨。
Promise 物件的設計就是針對異步函式的執行結果所設計的,promise 物件最後的結果要不然就用一個回傳值來 fulfilled(實現),要不然就用一個理由(錯誤)來 rejected(拒絕)。
你可能會認為這種用失敗(或拒絕)或成功的兩分法結果,似乎有點太武斷了,但在許多異步的結構中,的確是用成功或失敗來作為代表,例如 AJAX 的語法結構。promise 物件用實現(解決)與拒絕來作為兩分法的分別字詞。對於有回傳值的情況,沒有什麼太多的考慮空間,必定都是實現狀態,但對於何時才算是拒絕的狀態,這有可能需要仔細考量,例如以下的情況:
好的拒絕狀態應該是:
- I/O 操作時發生錯誤,例如讀寫檔案或是網路上的資料時,中途發生例外情況
- 無法完成預期的工作,例如
accessUsersContacts
函式是要讀取手機上的聯絡人名單,因為權限不足而失敗 - 內部錯誤導致無法進行異步的程序,例如環境的問題或是程式開發者傳送錯誤的傳入值
壞的拒絕狀態例如:
- 沒有找到值或是輸出是空白的情況,例如對資料庫查詢,目前沒有找到結果,回傳值是 0。它不應該是個拒絕狀態,而是帶有 0 值的實現。
- 詢問類的函式,例如
hasPermissionToAccessUsersContacts
函式詢問是否有讀取手機上聯絡人名單的權限,當回傳的結果是 false,也就是沒有權限時,應該是一個帶有 false 值的實現。
不同的想法會導致不同的設計,舉一個明確的實例來說明拒絕狀態的情境設計。jQuery 的ajax()
方法,它在失敗時會呼叫fail
處理函式,失敗的情況除了網路連線的問題外,它會在雖然伺服器有回應,但是是屬於失敗類型的 HTTP 狀態碼時,也算作是失敗的狀態。但另一個可以用於類似功能的 Fetch API 並沒有,fetch
使用 Promise 架構,只有在網路連線發生問題才會轉為 rejected(拒絕)狀態,只要是伺服器有回應都算已實現狀態。
註: 在 JavaScript 中函式的設計,必定有回傳值,沒寫只是回傳 undefined,相當於
return undefined