Promise教學 - 基本概念


本內容是從免費電子書從Promise開始的JavaScript異步生活中轉貼過來,有興趣可以前往閱讀。


異步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物件的設計就是針對異步函式的執行結果所設計的,要不就是用一個回傳值來變成已實現狀態,要不就是用一個理由(錯誤)來變成已拒絕狀態

同步程序你應該很熟悉了,大部份你寫的程式碼都是同步的程序。一步一步(一行一行)接著執行。異步程序有一些你可能用過,setTimeoutXMLHttpRequest(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