承諾是一種相對較新的處理異步的方式,它可以非常有助於結構化代碼。
警告:此帖子已經過時並且可能不反映最新的技術水平。
請查看我的Promises guide和我的async/await guide。
承諾是事件的對象表示方式。在其生命週期中,當調用時,承諾從待定狀態轉換為已解決或已拒絕狀態,或者它也可以永遠保持待定狀態而不解決。
它是一種對JavaScript事件的新方法,但我認為它生成的代碼更易讀且更少出錯。目前,在JavaScript中有兩種稍有不同的主要承諾實現:遵循Promises/A規範的庫和jQuery。
首先,我將考慮jQuery,因為它無處不在且我使用它,所以如果您不希望使用另外一個外部庫,可以使用它。
介紹jQuery Promise
讓我們介紹延遲的概念。首先,延遲是一個承諾,除了您可以觸發一個延遲(解決或拒絕它)之外,通過承諾,您只能添加回調,並且它將由其他東西觸發。如果您希望,承諾是延遲的“只收聽”部分。
這是一個清晰的例子:
var promise = $('div.alert').fadeIn().promise();
您現在可以添加.done()和.fail()來處理回調。這只是一個調用示例,使用承諾的動畫已經成為jQuery 1.8中的一個真正的東西,同時帶有進度的回調。
另一個例子是AJAX調用:
var promise = $.get(url);
promise.done(function(data) {});
延遲是您可以創建的東西,設置回調並解決的東西,例如:
var deferred = new $.Deferred();
deferred.done(function(data) { console.log(data) });
deferred.resolve('some data');
延遲的狀態可以使用.resolve()或.reject()觸發。一旦延遲狀態已經更改為最終階段(已解決/已拒絕),它就無法再改變。
var deferred = new $.Deferred();
deferred.state(); // "pending"
deferred.resolve();
deferred.state(); // "resolved"
我們可以將以下回調附加到一個承諾:
.done() // 當承諾成功執行時運行
.fail() // 當承諾失敗時運行
.always() // 無論什麼情況下都運行
可以使用.then()
一起調用這些回調,如下所示:
promise.then(doneFunc, failFunc, alwaysFunc);
這只是對承諾和延遲的jQuery實現的介紹。讓我們寫一些真實世界的例子。 (如果在node中執行,可以使用$ = require('jquery');
導入jQuery)
一些jQuery示例
例如,這裡我們執行一個函數,當它完成時調用dfd.resolve()。類似於使用回調,但更具結構性和可重用性。
$.when(execution()).then(executionDone);
function execution(data) {
var dfd = $.Deferred();
console.log('start execution');
//在真實世界中,這可能會進行AJAX調用。
setTimeout(function() { dfd.resolve() }, 2000);
return dfd.promise();
}
function executionDone(){
console.log('execution ended');
}
在這裡,陣列的元素被處理,並且當它們全部都正常(例如一個請求已返回)時,我調用另一個函數。我們開始看到使用Deferred使用的真正好處。使用$.when.apply()
方法在循環中分組了dfd.resolve()。
var data = [1,2,3,4]; // 來自serviceA的ID
var processItemsDeferred = [];
for(var i = 0; i < data.length; i++){
processItemsDeferred.push(processItem(data[i]));
}
$.when.apply($, processItemsDeferred).then(everythingDone);
function processItem(data) {
var dfd = $.Deferred();
console.log('called processItem');
//在真實世界中,這可能會進行AJAX調用。
setTimeout(function() { dfd.resolve() }, 2000);
return dfd.promise();
}
function everythingDone(){
console.log('processed all items');
}
稍微複雜一點的例子,這裡的陣列元素是從外部資源獲取的,使用fetchItemIdsDeferred = fetchItemIds(data)
和fetchItemIdsDeferred.done()
。
var data = []; // 來自serviceA的ID
var fetchItemIdsDeferred = fetchItemIds(data); // 必須將id添加到data中
function fetchItemIds(data){
var dfd = $.Deferred();
console.log('calling fetchItemIds');
data.push(1);
data.push(2);
data.push(3);
data.push(4);
setTimeout(function() { dfd.resolve() }, 1000);
return dfd.promise();
}
fetchItemIdsDeferred.done(function() { // 如果fetchItemIds成功...
var processItemsDeferred = [];
for(var i = 0; i < data.length; i++){
processItemsDeferred.push(processItem(data[i]));
}
$.when.apply($, processItemsDeferred).then(everythingDone);
});
function processItem(data) {
var dfd = $.Deferred();
console.log('called processItem');
//在真實世界中,這可能會進行AJAX調用。
setTimeout(function() { dfd.resolve() }, 2000);
return dfd.promise();
}
function everythingDone(){
console.log('processed all items');
}
最後兩個示例解釋了如何計算for循環,然後等待處理執行結束後做某些事情。
這是做這件事情最“hacky”的方式:
var allProcessed = false;
var countProcessed = 0;
for (var i = 0, len = theArray.length; i < len; i++) {
(function(i) {
// do things with i
if (++countProcessed === len) allProcessed = true;
})(i);
}
現在讓我們看Deferreds可以用於的另一個例子:看看這個
var interval = setInterval(function() {
if (App.value) {
clearInterval(interval);
// do things
}
}, 100);
這是一個評估條件的結構;如果條件為true,代碼將清除間隔並執行if內容。
這對於例如檢查當一個值不再是undefined時很有用:
var DeferredHelper = {
objectVariableIsSet: function(object, variableName) {
var dfd = $.Deferred();
var interval = setInterval(function() {
if (object[variableName] !== undefined) {
clearInterval(interval);
console.log('objectVariableIsSet');
dfd.resolve()
}
}, 10);
return dfd.promise();
},
arrayContainsElements: function(array) {
var dfd = $.Deferred();
var interval = setInterval(function() {
if (array.length > 0) {
clearInterval(interval);
console.log('arrayContainsElements');
dfd.resolve()
}
}, 10);
return dfd.promise();
}
}
var executeThis = function() {
console.log('ok!');
}
var object = {};
object.var = undefined;
var array = [];
$.when(DeferredHelper.arrayContainsElements(array)).then(executeThis);
$.when(DeferredHelper.objectVariableIsSet(object, 'var')).then(executeThis);
setTimeout(function() {
object.var = 2;
array.push(2);
array.push(3);
}, 2000);
上面的示例實際上是三個示例。我創建了一個DeferredHelper對象,它的方法arrayContainsElements和objectVariableIsSet不言自明。
請記住,基本類型按值傳遞,因此您不能這樣做:
var integerIsGreaterThanZero = function(integer) {
var dfd = $.Deferred();
var interval = setInterval(function() {
if (integer > 0) {
clearInterval(interval);
dfd.resolve()
}
}, 10);
return dfd.promise();
};
var variable = 0;
$.when(integerIsGreaterThanZero(variable)).then(executeThis);
或者你不能這樣做:
var object = null;
var variableIsSet = function(object) {
var dfd = $.Deferred();
var interval = setInterval(function() {
if (object !== undefined) {
clearInterval(interval);
console.log('variableIsSet');
dfd.resolve()
}
}, 10);
return dfd.promise();
};
$.when(variableIsSet(object)).then(executeThis);
setTimeout(function() {
object = {};
}, 2000);
這是因為當對像= {}時,對象參考被更改,而JavaScript實際上通過複製引用來引用變量,variableIsSet函數內部的對象變量的參考與外部對像變量不同。
一個ember.js示例
我在Ember.js中使用的一個功能是:
App.DeferredHelper = {
/**
* Check if an array has elements on the App global object if object
* is not set.
* If object is set, check on that object.
*/
arrayContainsElements: function(arrayName, object) {
var dfd = $.Deferred();
if (!object) object = App;
var interval = setInterval(function() {
if (object.get(arrayName).length > 0) {
clearInterval(interval);
dfd.resolve()
}
}, 50);
return dfd.promise();
},
/**
* Check if a variable is set on the App global object if object
* is not set.
* If object is set, check on that object.
*/
variableIsSet: function(variableName, object) {
var dfd = $.Deferred();
if (!object) object = App;
var interval = setInterval(function() {
if (object.get(variableName) !== undefined) {
clearInterval(interval);
dfd.resolve()
}
}, 50);
return dfd.promise();
}
}
因此,我可以在客戶端代碼中這樣做:
$.when(App.DeferredHelper.arrayContainsElements('itemsController.content'))
.then(function() {
//do things
});
和
$.when(App.DeferredHelper.variableIsSet('aVariable'))
.then(function() {
//do things
});
//&
$.when(App.DeferredHelper.variableIsSet('aVariable', anObject))
.then(function() {
//do things
});
這些示例都是使用jQuery延遲實現的。
如果您不想使用jQuery延遲實現,也許是因為您不使用jQuery並且僅為了延遲而加載它過多,或者您使用的其他庫沒有延遲實現,您可以使用其他專門用於此的庫,例如Q、rsvp.js、when.js。
讓我們使用when.js編寫一些例子。
使用when.js撰寫一些例子
例如,我有一個項目的ID,我想要調用API端點以獲取有關它的更多詳細信息。一旦AJAX調用返回,繼續處理。
function processItem(item) {
var deferred = when.defer();
var request = $.ajax({
url: '/api/itemDetails',
type: 'GET'
data: {
item: item
}
});
request.done(function(response) {
deferred.resolve(JSON.parse(response));
});
request.fail(function(response) {
deferred.reject('error');
});
return deferred.promise;
}
var item = {
id: 1
}
processItem(item).then(
function gotIt(itemDetail) {
console.log(itemDetail);
},
function doh(err) {
console.error(err);
}
);
我從服務器獲取了一些ID值,使用上面的processItem()
函數將它們處理,然後一旦完成處理所有項目,我可以做一些事情。
function processItems(anArray) {
var deferreds = [];
for (var i = 0, len = anArray.length; i < len; i++) {
deferreds.push(processItem(anArray[i].id));
}
return when.all(deferreds);
}
var anArray = [1, 2, 3, 4];
processItems(anArray).then(
function gotEm(itemsArray) {
console.log(itemsArray);
},
function doh(err) {
console.error(err);
}
);
when.js庫提供了一些實用的方法,例如when.any()和when.some(),讓延遲的回調在1)解決了其中一個承諾時或者2)至少指定數量的承諾返回時運行。