異步編程:When.js快速上手


when.js很小,壓縮后只有數kb,gzip后的大小幾乎可以忽略。在Node和瀏覽器環境里都可以使用when.js

首先,我們看一小段代碼:

var getData = function(callback) {
    $.getJSON(api, function(data){
        callback(data[0]);
    });
}

var getImg = function(src, callback) {
    var img = new Image();

    img.onload = function() {
        callback(img);
    };

    img.src = src;
}

var showImg = function(img) {
    $(img).appendTo($('#container'));
}

getData(function(data) {
    getImg(data, function(img) {
        showImg(img);
    });
});

這段代碼完成了三個任務:1)獲取數據;2)加載圖片;3)顯示圖片,其中,任務1和2是異步,3是同步,使用的是最常見的callback機制來處理異步邏輯,好處是淺顯易懂,缺點是強耦合、不直觀、處理異常麻煩等等。

我們嘗試用when.js改寫下這段代碼:

var getData = function() {
    var deferred = when.defer();

    $.getJSON(api, function(data){
        deferred.resolve(data[0]);
    });

    return deferred.promise;
}

var getImg = function(src) {
    var deferred = when.defer();

    var img = new Image();

    img.onload = function() {
        deferred.resolve(img);
    };

    img.src = src;

    return deferred.promise;
}

var showImg = function(img) {
    $(img).appendTo($('#container'));
}

getData()
.then(getImg)
.then(showImg);

看最后三行代碼,是不是一目了然,非常的語義化?來看下改寫后的任務1、2多了些什么:

var deferred = when.defer();

定義了一個deferred對象。

deferred.resolve(data);

在異步獲取數據完成時,把數據作為參數,調用deferred對象的resolve方法。

return deferred.promise;

返回了deferred對象的promise屬性。
在Promises/A規范中,每個任務都有三種狀態:默認(pending)、完成(fulfilled)、失敗(rejected)。

  • 默認狀態可以單向轉移到完成狀態,這個過程叫resolve,對應的方法是deferred.resolve(promiseOrValue);
  • 默認狀態還可以單向轉移到失敗狀態,這個過程叫reject,對應的方法是deferred.reject(reason);
  • 默認狀態時,還可以通過deferred.notify(update)來宣告任務執行信息,如執行進度;
  • 狀態的轉移是一次性的,一旦任務由初始的pending轉為其他狀態,就會進入到下一個任務的執行過程中。

有人可能會覺得奇怪:改變任務狀態的resolve和reject方法是定義在deferred對象上,但最后返回的卻是deferred的promise屬性。這么做一是因為規范就是這么定的,二是可以防止任務狀態被外部改變。
then有三個參數,分別是onFulfilled、onRejected、onProgress,通過這三個參數,就可以指定上一個任務在resolve、reject和notify時該如何處理。例如上一個任務被resolve(data),onFulfilled函數就會被觸發,data作為它的參數;被reject(reason),那么onRejected就會被觸發,收到reason。任何時候,onFulfilled和onRejected都只有其一可以被觸發,並且只觸發一次;onProgress顧名思義,每次notify時都會被調用。下面是reject和notify的用法:

function run() {
    var deferred = when.defer();
    var start = 1, end = 100;

    (function() {
        if(start <= end) {
            deferred.notify(start++);
            setTimeout(arguments.callee, 50);
        } else {
            deferred.reject('time out!');
        }
    })();

    return deferred.promise;
}

run().then(undefined,
    function(reason) {
        alert(reason);
    }, function(s) {
        document.getElementById('output').innerHTML = s + '%';
    }
);

then會傳遞錯誤,也就是說有多個任務串行執行時,我們可以只在最后一個then定義onRejected。只定義了onRejected的then等同於otherwise,也就是說 otherwise(onRejected) 是 then(undefined, onRejected) 的簡便寫法。
then會在try..catch..的包裹之下執行任務,所以任務的異常都會被when.js捕獲,當做失敗狀態處理,類似這樣:

try {
    ...
} catch (e) {
    deferred.reject(e);
}

在任務狀態改變之后再then,依然可以正常工作,后續任務會立刻執行。如果要在多個任務最后做cleanup工作,而不管之前的任務成功與否,可以用ensure方法。它只接受一個參數onFulfilledOrRejected,始終會執行。另外when.js還有一個always方法,即將廢棄,建議大家不要使用。
回到上面加載圖片的場景,如果把任務2變為:加載多張圖片,全部完成后再執行任務3。這時候需要用到when.all,when.all接受一個promise數組,返回promise,這個promise會在promise數組中每一個promise都resolve之后再resolve。說起來拗口,看代碼就明白了:

var getData = function() {
    var deferred = when.defer();

    $.getJSON(api, function(data){
        var data = data.slice(0, 3);
        deferred.resolve(data);
    });

    return deferred.promise;
}

var getImg = function(src) {
    var deferred = when.defer();

    var img = new Image();

    img.onload = function() {
        deferred.resolve(img);
    };

    img.src = src;

    return deferred.promise;
}

var showImgs = function(imgs) {
    $(imgs).appendTo($('#container'));
}

var getImgs = function(data) {
    var deferreds = [];
    for(var i = 0; i < data.length; i++) {
        deferreds.push(getImg(data[i]));
    }
    return deferreds;
}

when.all(getData().then(getImgs)).then(showImgs);

如果我們只是想把一個promise數組挨個執行一遍,可以用when.settle:

var promise1 = function() {
    var deferred = when.defer();
    setTimeout(function() {
            deferred.reject('A');
        }, 2000);
    return deferred.promise;
};

var promise2 = function() {
    var deferred = when.defer();
    setTimeout(function() {
            deferred.resolve('B');
        }, 2000);
    return deferred.promise;
};

when.settle([promise1(), promise2()]).then(function(result) {
    console.log(result); /*
    [{"state":"rejected","reason":"A"},
    {"state":"fulfilled","value":"B"}] */ 
});

有時候,我們需要引入任務競爭機制,例如從一批cdn中找到最快的那個,when.any就派上用場了,when.any接受promise數組,在其中任何一個resolve后就接着執行后續任務了。如果要在一批promise中某幾個resolve后執行后續任務,可以用when.some,它比when.any多一個howMany的參數。
Promise給異步編程代碼帶來了巨大的方便,從此我們可以更專注單個任務的實現,promise會很好的替我們解決任務調度問題。when.js提供的功能遠遠不止本文提到的這些,有興趣的同學可以前往官方api文檔了解更多。

github:https://github.com/cujojs/when#legacy-environments
via:Jerry Qu
本文鏈接:https://imququ.com/post/promises-when-js.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM