在談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 的實作範例嗎? 不是知名套件的。
回覆刪除