在談Promise 之前,我們先來了解一下 CommonJS.org
CommonJS.org
CommonJS 是一個致力於將 JavaScript 生態圈標準化的計畫,尤其是針對瀏覽器以外的應用環境,也因此第一個版本的名字叫做 ServerJS。CommonJS 的終極目標是制定一個像 C++ 標準庫一樣的規範,也就是介面,使得基於 CommonJS API 的應用程序可以在不同的環境下運行,就像用 C++ 編寫的應用程序可以使用不同的編譯器和運行時函數庫一樣。為了保持中立,CommonJS 不參與函式庫的實作,而是交給像 Node.js 之類的項目來完成。
而不少開發者向 CommonJS 提出 Promise 的規範草案,但是一直未被列入正式規範,有著 Promises/A, B, KISS, C, D 等版本。其中以 Promises/A 的提案最為簡單,也最常被大家提及。
值得一提的是:Promise/B, D 的提案人是知名流程控管套件 Q.js 的作者 Kris Kowal
滿有趣的一點是:Node 團隊在前年左右放棄繼續遵守 CommonJS 的規範。Isaac 指出 "Ryan basically always gave zero fucks about CommonJS anyway" ,並轉述了 Ryan 的一句重話 : "Forget CommonJS. It's dead. We are server side JavaScript."
有興趣可以看看這個Github的討論串:https://github.com/joyent/node/issues/5132#issuecomment-15432598
( Issac 是 NPM 的作者, Ryan 是 Node 之父 )
什麼是Promise?
根據 Promises/A 規範草案,Promise物件有以下特性:- 有三種狀態 - unfulfilled, fulfilled, failed,且只能從 unfulfilled 變為 fulfilled, failed (狀態機的概念)
- 本身具有一個名叫 then 的 method
- then 接受三個參數 - fulfilledHandler (執行成功後被呼叫的callback), errorHandler (執行失敗後被呼叫的callback), progressHandler (執行成功後被呼叫的callback)
- then 被執行之後,必須回傳另一個 Promise 物件
- fulfilledHandler / failedHandler 被執行以後回傳的值,將被作為 then 回傳的 promise 物件的 fulfilledHandler / failedHandler 或 的 input。簡單來說,每一步的回傳值會被當做下一步的 input
- Promise 的狀態 "必須" 被改變
(請注意:這個函式沒有實作 notify / progress 機制)
有了Promises,我們可以將如此的嵌套地獄 (俗稱末日金字塔):
step1(function (value1) { step2(value1, function(value2) { step3(value2, function(value3) { step4(value3, function(value4) { // Do something with value4 }); }); }); });
改寫成這樣:(使用Q.js)
Q.fcall(promisedStep1) .then(promisedStep2) .then(promisedStep3) .then(promisedStep4) .then(function (value4) { // Do something with value4 }) .catch(function (error) { // Handle any error from all above steps }) .done();
總而言之,Promises最重要的精神就是:
"作為將來未知的回傳值的代理"
(A promise serves as a proxy for a future value)
以Promises/A為依據,Q.js 的核心成員在後來提出了更為嚴謹的 Promises/A+
什麼是Deferred?
Promises是流程控管的實作介面,有著較明確的規範。而 Deferred 就比較虛無飄渺了,有些時候他被看做是Promise的別名,有時候被用來產生Promise物件。而比較常見的實作是:Deferred作為pomise物件的控制者,負責 resolve (將狀態從unfulfilled切換成fulfilled) 或 reject (將狀態從unfulfilled切換成failed) 自己的Promise物件,為了保護 promise 的封閉性,promise物件是不應該resolve自己的。他們的關係大致可以用下圖呈現:
在 jQuery 和 Q.js 都有類似的實作,我們來看看 Q.defer() 的範例:
function requestOkText(url) { var request = new XMLHttpRequest(); var deferred = Q.defer(); request.open("GET", url, true); request.onload = onload; request.onerror = onerror; request.onprogress = onprogress; request.send(); function onload() { if (request.status === 200) { deferred.resolve(request.responseText); } else { deferred.reject(new Error("Status code was " + request.status)); } } function onerror() { deferred.reject(new Error("Can't XHR " + JSON.stringify(url))); } function onprogress(event) { deferred.notify(event.loaded / event.total); } return deferred.promise; } requestOkText("http://localhost:3000") .then(function (responseText) { // If the HTTP response returns 200 OK, log the response text. console.log(responseText); }, function (error) { // If there's an error or a non-200 status code, log the error. console.error(error); }, function (progress) { // Log the progress as it comes in. console.log("Request progress: " + Math.round(progress * 100) + "%"); });
總結
Promise 是一個有具體定義的流程控制介面,而 Defer 不是。常見的架構是由 Defer 物件解析(resolute)所屬的 Promise 物件,來切換 promise 的狀態,也因此 Defer.resolve/reject 往往會在 Async Callback Function 中被呼叫,例如當成功取得遠端回應時,呼叫 deferred.resolve(data),將 deferred.promise 的狀態改為 "fulfilled"(成功),繼續呼叫promise鏈中的下一個 fulfilledHandler。
而 promise 比起 defer 物件,是較公開、常被傳來傳去的 (defer物件往往藏在匿名 callback function 中,只有上下文才能存取到它),也因為這樣的特性,Promise物件不宜設計為能夠自我解析(resolve/reject自己)的機制,而是由較封閉的 defer物件來解析它,已確保狀態機的穩定。
而 Promise 物件具有 then 方法,這個方法回傳的還是 promise 物件,如此一來可以形成一串 promise 鏈,依不同的狀態一一呼叫對應的 Handler。
Reference
- CommonJS/Promises
- Promise & Deferred objects in JavaScript Pt.1: Theory and Semantics.
- You're Missing the Point of Promises <中文版>
- Promise 初探 - by MWEB
- JavaScript中的异步梳理(2)— 使用Promises/A - by MWEB
有native javascript 的實作範例嗎? 不是知名套件的。
回覆刪除