注:本文是譯文,難免有錯誤或理解不足之處,請大家多多指正,大家也可挪步原文。由於本文講解十分精彩,非常推薦大家查看原文,由於原文內容十分豐富,所以將其分爲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/
基本用法
首先,讓我們先創建一個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。
和
function getDataFromBackend(query) {
var data = searchInCache(query);
if (data) {
return $q.when(data);
} else {
return makeAsyncBackendCall(query);
}
}
getDataFromBackend()函數用來從後臺獲取數據,不過在訪問後臺之前,先要在本地緩存中查找是否有相關的數據,如果有就使用$q.when()返回一個resolved promise。
例如,jQuery的
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裏的常用的方法,我想現在可以進行教程練習了。