Promise - Q函式庫


前言

Promise(承諾)已經確定是下一代Javascript的ES6標準。在這之前,已經有數套相關的函式庫支援這個新的特性。這篇文章是接著jQuery中的Promise而寫來的,從jQuery的Deferred概念來理解另一套知名的函式庫 - Q。

這篇文章主要還是簡介,深入的應用還是請再到Q的網站上觀看相關的API文件。

Q

Q是知名與功能完整的Promise函式庫,當然它已經實作了Promise/A+標準。

特點:

  • 發展早,較其他函式庫穩定成熟。
  • Q可以在瀏覽器端的函式庫或框架(jQuery, AngularJS)配合使用,也可以用於伺服器端的Node.js之中。
  • 提供多種方便工具,可以讓Promise-based的程式碼更容易寫,或是打包(轉換)非Promise-based的函式
  • 提供可以用Promise建立控制流程的工具函式庫

Github網站: Q

從jQuery的Deffered開始

Q中也有Deffered物件的設計,但與jQuery的Deffered機制有很多明顯的不同,它們可以配合使用,先了解一下比較明顯的差異。

done與fail

對應jQuery中的donefail方法,在Q中則是使用then方法,這是Promise標準的方法:

//jQuery Example
jQueryPromise.done(success).fail(failure);
//Q Example
qPromise.then(success, failure);

Q中的有一個方法稱為catch,它其實就是相等於jQuery中fail方法。但Q中的done方法,和jQuery中的done方法的意義和用途不同,Q中的done,是最後一個then,代表終了、串連完成。以下是Q的程式範例:

var promiseB = qPromiseA.then(makeB);
var promiseC = qPromiseA.then(makeC);
qPromiseA.catch(handleFailureA).done();

上面的程式範例也有一個特性是jQuery中沒有的,就是可以依附(attach)多個處理函式(handlers)在同一個Promise上。這意思是說Q除了在chain(串連)機制之外,可以達成更複雜的"控制流程(control flow)"。

我需要在接在後面then裡的函式回傳Promise物件嗎(例如上面的makeB、makeC)?正確答案是"不用",then裡的函式回傳值會自動幫你打包成Promise。

Defered物件

在jQuery中,如果要讓我們自已寫的函式方法能回傳Promise,就得用到Defered物件:

//jQuery Example
function foo() {
    var defered = $.Deferred();
    defered.resolve(10);
    return defered.promise();
}

在Q中也提供了類似的deferred物件:

//Q Example
function foo() {
    var defered = Q.defer();
    defered.resolve(10);
    return defered.promise;
}

這兩者雖然語法看來很像,內部機制卻不太一樣,Promise物件的設計也不同。Q提供這種類似語法的用意,大概是想讓熟悉jQuery的開發者能更輕鬆移轉到Q來。

要特別注意的是對Q中的deferred物件而言,Promise是它的屬性。而對jQuery的deferred物件而言,則是透過promise()回傳Promise物件。

其他重要的對應方法

在Q的說明文件中,最下面也列出一張表,對應jQuery中有關於Deferred物件所提供的方法,其中有兩個比較重要常見的,如果有需要可以參照看看:

  • jQuery: always vs Q: finally
  • jQuery: $.when vs Q: Q或Q.all

Q中獨有

上面說明的是從jQuery開發者的角度來看,實際上Q並不是單純的只用來取代jQuery中的Deferred機制而已。Q是一個發展已經很完整成熟的Promise應用函式庫,以下說說它有什麼獨有的地方。

轉換(打包)用工具

在Q中並不是只有用Deferred物件,才能建立會回傳Promise物件的函式,它還提供了別的方便使用的方式(工具)。

使用 Q.fcall

Q.fcall方法可以直接轉換某個函式回傳為Promise物件,十分簡便的工具:

return Q.fcall(function () {
    return 10;
});

用了Q.fcall之後,就可以有這麼清楚的流程:

Q.fcall(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function(value4){
    //do something with value4
}, function(error){
    //handle any error from step through step4
})
.done();

fcall在後面可以加上可變個數(Variadic)的參數,這些參數是給第1個參數的函式所用的,例如q.fcall(JSON.parse, "[1,2,3,4]")的用法。

使用 Q.Promise(或簡寫為Q)

這個方法與Deferred的用法有些類似,主要是要有三個方法resolve, reject, notify寫明Promise在對應的狀態要作什麼:

function requestOkText(url) {
    return Q.Promise(function(resolve, reject, notify) {
        var request = new XMLHttpRequest();

        request.open("GET", url, true);
        request.onload = onload;
        request.onerror = onerror;
        request.onprogress = onprogress;
        request.send();

        function onload() {
            if (request.status === 200) {
                resolve(request.responseText);
            } else {
                reject(new Error("Status code was " + request.status));
            }
        }

        function onerror() {
            reject(new Error("Can't XHR " + JSON.stringify(url)));
        }

        function onprogress(event) {
            notify(event.loaded / event.total);
        }
    });
}

notify是用在提供例如上傳檔案進度的訊息通知用的,這並不是Promise/A標準中定義的方法。

它也可以轉換jQuery的Promise回傳物件,變成Q的Promise回傳物件:

return Q(jQuery.ajax({
    url: "foobar.html", 
    type: "GET"
})).then(function (data) {
    // on success
}, function (xhr) {
    // on failure
});

// Similar to jQuery's "complete" callback: return "xhr" regardless of success or failure
return Q.promise(function (resolve, reject) {
    jQuery.ajax({
        url: "foobar.html",
        type: "GET"
    }).then(function (data, textStatus, jqXHR) {
        delete jqXHR.then; // treat xhr as a non-promise
        resolve(jqXHR);
    }, function (jqXHR, textStatus, errorThrown) {
        delete jqXHR.then; // treat xhr as a non-promise
        reject(jqXHR);
    });
});

於Node.js中使用

Q可以應用在Node.js之中。Node.js的內建方法(都是error-first callback),目前的版本並不會回傳Promise物件,Q可以轉換它們為回傳Promise,對比上面的Q.fcall,Node.js中有專用的Q.nfcall方法:

return Q.nfcall(FS.readFile, "foo.txt", "utf-8");

轉換的工具方法很多,例如直接把一個原本的Node.js方法轉成另一個具有回傳Promise能力的方法,這是使用Q.nfbind方法(或是Q.denodeify,這是它的別名):

var fs_readFile = Q.nfbind(FS.readFile);

fs_readFile("foo.txt", "utf-8").done(function (text) {

});

上面的Q.nfbind方法它作的事情,和之前說的用Deffered物件建構自訂函式一樣,像下面的範例這樣,只是上面只需要呼叫一個方法,就會幫你作完所有該作的事:

function fs_readFile (file, encoding) {
  var deferred = Q.defer()
  fs.readFile(file, encoding, function (err, data) {
    if (err) deferred.reject(err) // rejects the promise with `err` as the reason
    else deferred.resolve(data) // fulfills the promise with `data` as the value
  })
  return deferred.promise // the promise is returned
}

錯誤與例外處理(Errors & Exception Handling)

異步函式最麻煩的其中一件事,就是關於錯誤和例外的處理。在Node.js的error-first callbacks中是不能使用try...throw這種"同步"的語法敘述,所以對於除錯時就會更麻煩。不過如果改用Promise結構後就可以使用了,這也是Promise的強大之處 - 可以讓你在異步的流程中使用同步的敘述或函式,範例如下:

var promise2 = promise1.then(function () {
    throw new Error("boom!");
});

比對一下下面的try...throw語法敘述:

try {
  doSomethingRisky();
  doAnotherRiskyThing();
} catch (e) {
  console.log(e);
}

而具有同功效的Promise敘述寫起來非常的直覺:

doSomethingRisky()
.then(doAnotherRiskyThing)
.then(null, console.log);

jQuery中的Promise目前在版本2.0中因為實作的Promise不完全符合標準,所以例外處理的機制並不太一樣。

Promise的錯誤處理機制通常是擺到最後面,而且是需要"往後處理",以下面這個範例來說,你可能會覺得怎麼不把doAnotherRiskyThingdoErrorOrException放在同一個then裡面?答案是不行的,因為當doAnotherRiskyThing在throw(丟)出一個錯誤時,是回傳含在一個Promise中,交往下一個then去處理:

doStuff()
.then(doAnotherRiskyThing)
.then(null, doErrorOrException);

記得:每個Promise只能resolve(或reject)一次

控制流程(Control Flow)

最簡單也是最常見的流程是使用then/catch/done的順序,讓異步的callbacks按順序一個個執行和串連在一起,這稱為"Sequences(按照先後次序)"的流程:

Q.fcall(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function(value4){
    //do something with value4
}, function(error){
    //handle any error from step through step4
})
.done();

第二種是稱為Parallel(平行、併行)的流程,是讓所有的把多個Promise同時間執行,如果都成功則回傳一個Promise,這用Q.all方法就可以作得到:

這和jQuery中的$.when機制類似,Q官方文件把它稱為Combination(合併)

var allPromise = Q.all([ fs_readFile('file1.txt'), fs_readFile('file2.txt') ]);
allPromise.then(console.log, console.error);

在Q中還有提供更進階的流程控制,例如reduceallSettledspread三個方法,可以到它的Github網站中看到API的說明

參考資料