Angular(五)Promise

注:本文是譯文,難免有錯誤或理解不足之處,請大家多多指正,大家也可挪步原文。由於本文講解十分精彩,非常推薦大家查看原文,由於原文內容十分豐富,所以將其分爲2部分,這是Part 1(基礎篇),戳這裏查看Part 2(教程篇)

promise或deferred在異步編程中簡單而又實用。維基上列了一些promise模式的實現要點。AngularJS根據Kris Kowal’s Q 定義了了自己的實現方式。在本文中我將介紹promises和使用promises的目的,並且提供一個有關AngularJS $q Service的使用教程。

使用Promise (Deferred)的目的

JavaScript中使用回調函數來通知一個操作“成功”或“失敗”的狀態。例如,Geolocation api爲了獲取當前位置需要一個成功回調函數和一個失敗回調函數:

Geolocation api使用回調函數:

function success(position) {  
  var coords = position.coords;
  console.log('Your current position is ' + coords.latitude + ' X ' + coords.longitude);
}

function error(err) {  
  console.warn('ERROR(' + err.code + '): ' + err.message);
}

navigator.geolocation.getCurrentPosition(success, error);

另一個常見的例子是XMLHttpRequest(用來進行ajax調用)。XMLHttpRequest對象的onreadystatechange回調函數會在其readyState屬性值改變時被調用:

XHR使用回調函數:

var xhr = new window.XMLHttpRequest();  
xhr.open('GET', 'http://www.webdeveasy.com', true);  
xhr.onreadystatechange = function() {  
    if (xhr.readyState === 4) {
        if (xhr.status === 200) {
            console.log('Success');
        }
    }
};
xhr.send();

JavaScript異步編程中,類似的例子不勝枚舉,但是需要同步使用多個異步操作時使用回調函數的方式就不合適了。

嵌套噩夢(依次執行)

假設我們有N個異步方法:async1(success, failure), async2(success, failure), …, asyncN(success, failure),現在我們想要他們依次執行,後一個需要在前一個方法的success回調中才能執行,每一個函數都有success回調和failure回調:

async1(function() {  
    async2(function() {
        async3(function() {
            async4(function() {
                ....
                    ....
                        ....
                           asyncN(null, null);
                        ....
                    ....
                ....
            }, null);
        }, null);
    }, null);
}, null);

這樣我們就遇到了著名的“嵌套噩夢”。即使上述代碼有更好的表達方式,這樣的代碼也是難以閱讀和維護的。

平行執行

假設我們有N個異步方法:async1(success, failure), async2(success, failure), …, asyncN(success, failure),並且我們想讓他們平行執行,他們之間的執行是獨立的,他們都執行完成後,我們彈出一條消息。每一個方法都有自己的success回調和failure回調:

var counter = N;

function success() {  
    counter --;
    if (counter === 0) {
        alert('done!');
    }
}

async1(success);  
async2(success);  
....
....
asyncN(success);

我們首先聲明瞭一個計數器,並將其初始值設爲異步函數的個數,即N。當一個函數執行後,我們將計數器減1,並檢測其是否是最後一個執行的函數。這種方式並不容易實現和維護,尤其是當每個函數都給success回調傳參數。在這種情況下,我們需要保存函數每一次執行的結果。

在上面兩個例子中,異步函數執行時,我們都必須指定success回調的處理方式。換句話說,當我們使用回調函數時,異步操作需要保留他們的引用,但是保留的引用可能並不屬於我們的業務邏輯,這就提高了模塊和service之間的耦合度,使代碼的重用和測試變的複雜。

promise和deferred是什麼?

deferred表示一次異步操作的結果,它對外的接口用於表示此次異步操作的狀態和結果。通過它還能獲得其對應的promise實例。

promise提供了與deferred通信的接口,從而外部能夠通過promise得知deferred操作的狀態和結果。

當deferred被創建時,其狀態爲“掛起(pending)”,並沒有任何結果,當其被resolve()或reject()後,其狀態變爲“處理成功(resolved)”或“處理失敗(rejected)”。我們甚至可以在deferred剛剛被創建後就可以獲得其對應的promise實例,並且使用其完成某些功能。不過這些功能只有在deferred被resolve()或reject()後才能生效。

即使在我們還沒想好要在deferred被resolve()或reject()之後需要做什麼工作,我們也可以使用promise輕易創建一個異步操作。這就實現了低耦合。由於一個異步操作完成後不知道下一步應該做什麼,所以它必須在完成後發出信號。

deferred可以改變一個異步操作的狀態,而promise只能獲取和查看這些狀態,並不能改變狀態。這就是爲什麼一個函數通常應該返回promise而不是deferred的原因,這樣做使得外部的業務邏輯不能干涉異步操作的過程和狀態。

在不同的編程語言(JavaScript, Java, C++, Python等)和框架(NodeJS, jQuery等)中對於promise的實現均不相同。AngularJS在$q Service的基礎上實現promise。

怎樣使用deferred和promise

通過上文了解了promise和deferred的含義和用途後,下面讓我們來了解一下如何使用它們。如上文所說,promise的實現多種多樣,不同的實現方式具有不同的用法,這部分內容會使用[AngularJS的實現方式](https://docs.angularjs.org/api/ng/service/q) q Service。如果你使用的其他的實現方式也不用擔心,我在本文中提到的大部分方法都是通用的,如果不是,總有相同功能的方法。

基本用法

首先,讓我們先創建一個deferred:

var myFirstDeferred = $q.defer();
1
1
再簡單不過了,myFirstDeferred就是一個deferred,可以在異步操作結束後被resolve()或reject()。假設我們有個異步函數async(success, failure),參數爲success回調和failure回調,當async函數執行完畢後,我們希望對myFirstDeferred進行resolve()或reject()操作:

async(function(value) {  
    myFirstDeferred.resolve(value);
}, function(errorReason) {
    myFirstDeferred.reject(errorReason);
});

由於AngularJS的$q Service不依賴於上下文的執行環境,上面的代碼可以簡寫成:

async(myFirstDeferred.resolve, myFirstDeferred.reject);

得到myFirstDeferred的promise實例,並對其分配成功回調和失敗回調是非常簡單的:

var myFirstPromise = myFirstDeferred.promise;

myFirstPromise  
    .then(function(data) {
        console.log('My first promise succeeded', data);
    }, function(error) {
        console.log('My first promise failed', error);
    });

請注意,即使我們的異步函數async()還沒有被執行,只要我們獲得了deferred實例並得到其對應的promise,我們就可以對promise分配回調函數了:

var anotherDeferred = $q.defer();  
anotherDeferred.promise  
    .then(function(data) {
        console.log('This success method was assigned BEFORE calling to async()', data);
    }, function(error) {
        console.log('This failure method was assigned BEFORE calling to async()', error);
    });

async(anotherDeferred.resolve, anotherDeferred.reject);

anotherDeferred.promise  
    .then(function(data) {
        console.log('This ANOTHER success method was assigned AFTER calling to async()', data);
    }, function(error) {
        console.log('This ANOTHER failure method was assigned AFTER calling to async()', error);
    });

如果async()執行成功了(resolve被執行),上面代碼中的兩個“成功回調”都會被執行,async()執行失敗了(reject被執行),上面代碼中的兩個“失敗回調”也都會被執行。

封裝異步操作的一個好方法是定義一個返回promise的函數,這樣調用者可以按需要分配成功或失敗回調,而不能干涉或改變異步操作的狀態:

function getData() {  
    var deferred = $q.defer();
    async(deferred.resolve, deferred.reject);
    return deferred.promise;
}
...
... // Later, in a different file
var dataPromise = getData()  
...
...
... // Much later, at the bottom of that file :)
dataPromise  
    .then(function(data) {
        console.log('Success!', data);
    }, function(error) {
        console.log('Failure...', error);
    });

直到現在,我們使用promise時還是分配了成功回調和失敗回調,但其實也可以只分配成功回調或只分配失敗回調:

promise.then(function() {  
    console.log('Assign only success callback to promise');
});

promise.catch(function() {  
    console.log('Assign only failure callback to promise');
    // This is a shorthand for `promise.then(null, errorCallback)`
});

只傳遞成功回調給promise.then()就實現了“對promise只分配成功回調”,只傳遞失敗回調給promise.catch()就實現了“對promise只分配失敗回調”,而promise.catch()其實調用的是promise.then(null, errorCallback)。
而如果我們想要在deferred被resolve()和reject()後都做某些工作呢?我們可以使用promise.finally():

promise.finally(function() {  
    console.log('Assign a function that will be invoked both upon success and failure');
});

上述代碼其實和下面的代碼是等價的:

var callback = function() {  
    console.log('Assign a function that will be invoked both upon success and failure');
};
promise.then(callback, callback);

值和promise的鏈式操作

設想我們有個異步函數async()返回一個promise,有下面一段有趣的代碼:

var promise1 = async();  
var promise2 = promise1.then(function(x) {  
    return x+1;
});

很容易理解,promise1.then()返回了另一個promise,這裏命名爲promise2,當promise1被處理(x作爲參數傳入),在promise1的成功回調中返回了x+1,這時promise2對應的處理函數將接收x+1作爲參數。

再看一個類似的例子:

var promise2 = async().then(function(data) {  
    console.log(data);
    ... // Do something with data
    // Returns nothing!
});

當async()函數返回的promise被處理,其成功回調函數沒有返回任何值,那此時promise2對應的處理函數將接收到undefined。

上面可以看出,promise可以進行鏈式合成,並且上一個promise的處理結果將作爲下一個promise的處理參數。

爲了演示效果,下面使用一個很傻的使用promise的例子(沒有必要使用promise):

// Let's imagine this is really an asynchronous function
function async(value) {  
    var deferred = $q.defer();
    var asyncCalculation = value / 2;
    deferred.resolve(asyncCalculation);
    return deferred.promise;
}

var promise = async(8)  
    .then(function(x) {
        return x+1;
    })
    .then(function(x) {
        return x*2;
    })
    .then(function(x) {
        return x-1;
    });

promise.then(function(x) {  
    console.log(x);
});

這個promise鏈起始於async(8)的調用,async(8)返回的promise的成功回調的參數爲4,這個參數4以及對其處理結果會在所有promise的成功回調中傳遞,所以最後打印出的結果將是(8/2+1)*2-1,即爲9。

如果我們的鏈中傳遞的不是值,而是另一個promise,會發生什麼呢?假設現在我們有2個異步回調函數:async1()和async2(),它們都返回promise。來看下面的情形:

var promise = async1()  
    .then(function(data) {
        // Assume async2() needs the response of async1() in order to work
        var async2Promise = async2(data);
        return async2Promise;
});

不像上一個例子,這裏async1()返回的promise的成功回調中執行了另一個異步操作並返回了一個promise:async2Promise。意料之中async1.then()返回的是一個promise,但是其結果要根據async2Promise的執行結果來看了,async2Promise可能執行成功回調,也可能執行失敗回調。
因爲因爲async2()的參數使用的是async1()函數處理的值,並且async2()也返回一個promise,那上面的代碼可以簡寫成:

var promise = async1()  
    .then(async2);

下面是另一個例子,同樣也僅是用作演示:

// Let's imagine those are really asynchronous functions
function async1(value) {  
    var deferred = $q.defer();
    var asyncCalculation = value * 2;
    deferred.resolve(asyncCalculation);
    return deferred.promise;
}
function async2(value) {  
    var deferred = $q.defer();
    var asyncCalculation = value + 1;
    deferred.resolve(asyncCalculation);
    return deferred.promise;
}

var promise = async1(10)  
    .then(function(x) {
        return async2(x);
    });

promise.then(function(x) {  
    console.log(x);
});

首先我們調用了async1(10),async1函數對參數進行處理後(即resolve()操作)在其返回的promise的成功回調中傳入的參數x爲20,並執行了async2(20),而async2函數中同樣對參數進行處理後返回promise,此時async2返回的promise成功回調中傳入的參數將爲21,所以最後打印的結果爲21。

上述代碼可以用下面可讀性更強的表達方式:

function logValue(value) {  
    console.log(value);
}

async1(10)  
    .then(async2)
    .then(logValue);

這樣很容易看出執行的流程。

上面這些關於promise鏈的例子的結果是我們樂觀處理的結果,即:我們假設promise執行的是都是成功的回調函數,即deferred都被resolve()了。但是如果deferred被reject()了,那整個promise鏈都將被rejected:

// Let's imagine those are really asynchronous functions
function async1(value) {  
    var deferred = $q.defer();
    var asyncCalculation = value * 2;
    deferred.resolve(asyncCalculation);
    return deferred.promise;
}
function async2(value) {  
    var deferred = $q.defer();
    deferred.reject('rejected for demonstration!');
    return deferred.promise;
}

var promise = async1(10)  
    .then(function(x) {
        return async2(x);
    });

promise.then(  
    function(x) { console.log(x); },
    function(reason) { console.log('Error: ' + reason); });

很容易看出,最後打印的結果是Error: rejected for demonstration!,下面是一個關於promise鏈更高級的表示方法:

async1()  
    .then(async2)
    .then(async3)
    .catch(handleReject)
    .finally(freeResources);

這裏,我們依次調用了async1(),async2(),async3()函數,如果其中某個函數被reject(),那麼整個成功回調的鏈條將被打破,此時將執行handleReject()函數。而最後,不論怎樣,freeResources()函數都會被執行。例如,如果async2()中被reject(),那麼async3()將不會執行,handleReject()將接收async2()中reject()傳入的參數(也可能不傳參數)然後執行,最後執行freeResources()函數。

常用方法

AngularJS $q Service有一些非常有用的方法,這些方法在使用promise的時候會幫助很大。就像我開始所說的,其他的promise實現方式也有類似的方法,可能只是函數名不同。

有時我們需要返回一個被rejected的promise,我們可以使用$q.reject()返回一個帶有參數的rejected promise:

var promise = async().then(function(value) {  
        if (isSatisfied(value)) {
            return value;
        } else {
            return $q.reject('value is not satisfied');
        }
    }, function(reason) {
        if (canRecovered(reason)) {
            return newPromiseOrValue;
        } else {
            return $q.reject(reason);
        }
    });

如果async()返回的promise的成功回調函數接收的參數(即deferred.resolve(value);中傳遞的參數)是合適的值(isSatisfied()函數返回true),那這個參數將被promise鏈接收並被resolve(),如果這個參數不是合適的值(isSatisfied()函數返回false),那$q.reject返回的rejected promise將被加入到promise鏈中,導致promise鏈被rejected。

如果async()返回的promise的失敗回調函數接收的參數(即deferred.reject(param);中傳遞的參數)是合適的值(canRecovered()函數返回true),那麼一個新值或promise將被加入到promise鏈中,如果這個參數不是合適的值(canRecovered()函數返回false),那$q.reject()返回的rejected promise將被加入到promise鏈中,導致promise鏈被rejected。

q.reject() q.when(),有時我們需要返回一個resolved promise,我們可以使用$q.when()返回一個帶參數的resolved promise:

function getDataFromBackend(query) {  
    var data = searchInCache(query);
    if (data) {
        return $q.when(data);
    } else {
        return makeAsyncBackendCall(query);
    }
}

getDataFromBackend()函數用來從後臺獲取數據,不過在訪問後臺之前,先要在本地緩存中查找是否有相關的數據,如果有就使用$q.when()返回一個resolved promise。

q.when()promisejQuerysDeferredAngularJS q promise。

例如,jQuery的.ajax()調jQuerypromise使AngularJS q promise:

var jQueryPromise = $.ajax({  
    ...
    ...
    ...
});
var angularPromise = $q.when(jQueryPromise);

有時候我們需要執行多個異步函數,不在意其執行順序,只想在它們都執行完成後得到通知,可以使用$q.all(promiseArr)幫助我們實現這個功能。假設我們有N個異步方法:async1(), …, asyncN(),都返回promise,下面的代碼只有當所有的操作都被resolved時才能打印出”done”:

var allPromise = $q.all([  
    async1(),
    async2(),
    ....
    ....
    asyncN()
]);

allPromise.then(function(values) {  
    var value1 = values[0],
        value2 = values[1],
        ....
        ....
        valueN = values[N];

        console.log('done');
});

$q.all(promiseArr)當且僅當promiseArr數組裏面所有的promise都被resolve時返回resoloved promise。注意,只要有一個promise被rejected,那得到的結果將是rejected promise。

到此爲止,我們已經學習了怎樣創建一個deferred,怎麼對其進行resolve()和reject()操作,還學習怎樣對其promise進行操作。我們還了解了一些AngularJS $q Service裏的常用的方法,我想現在可以進行教程練習了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章