承諾是一種相對較新的處理異步的方式,它可以非常有助於結構化代碼。

警告:此帖子已經過時並且可能不反映最新的技術水平。

請查看我的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並且僅為了延遲而加載它過多,或者您使用的其他庫沒有延遲實現,您可以使用其他專門用於此的庫,例如Qrsvp.jswhen.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)至少指定數量的承諾返回時運行。

tags: JavaScript, Promises, Deferreds, jQuery, Ember.js, when.js