2015年2月18日 星期三

Deferred 與 Promise 的差異


在談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 的狀態 "必須" 被改變
根據這個定義,我寫了一個簡單的 Promise 函式:http://jsbin.com/poduza/6/edit?js,console
(請注意:這個函式沒有實作 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


沒有留言:

張貼留言