/

JavaScript中的延遲和承諾(+ Ember.js示例)

JavaScript中的延遲和承諾(+ Ember.js示例)

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

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

請查看我的Promises guide和我的async/await guide

承諾是事件的對象表示方式。在其生命週期中,當調用時,承諾從待定狀態轉換為已解決或已拒絕狀態,或者它也可以永遠保持待定狀態而不解決。

它是一種對JavaScript事件的新方法,但我認為它生成的代碼更易讀且更少出錯。目前,在JavaScript中有兩種稍有不同的主要承諾實現:遵循Promises/A規範的庫和jQuery。

首先,我將考慮jQuery,因為它無處不在且我使用它,所以如果您不希望使用另外一個外部庫,可以使用它。

介紹jQuery Promise

讓我們介紹延遲的概念。首先,延遲是一個承諾,除了您可以觸發一個延遲(解決或拒絕它)之外,通過承諾,您只能添加回調,並且它將由其他東西觸發。如果您希望,承諾是延遲的“只收聽”部分。

這是一個清晰的例子:

1
var promise = $('div.alert').fadeIn().promise();

您現在可以添加.done()和.fail()來處理回調。這只是一個調用示例,使用承諾的動畫已經成為jQuery 1.8中的一個真正的東西,同時帶有進度的回調。

另一個例子是AJAX調用:

1
2
var promise = $.get(url);
promise.done(function(data) {});

延遲是您可以創建的東西,設置回調並解決的東西,例如:

1
2
3
var deferred = new $.Deferred();
deferred.done(function(data) { console.log(data) });
deferred.resolve('some data');

延遲的狀態可以使用.resolve()或.reject()觸發。一旦延遲狀態已經更改為最終階段(已解決/已拒絕),它就無法再改變。

1
2
3
4
var deferred = new $.Deferred();
deferred.state(); // "pending"
deferred.resolve();
deferred.state(); // "resolved"

我們可以將以下回調附加到一個承諾:

1
2
3
.done() // 當承諾成功執行時運行
.fail() // 當承諾失敗時運行
.always() // 無論什麼情況下都運行

可以使用.then()一起調用這些回調,如下所示:

1
promise.then(doneFunc, failFunc, alwaysFunc);

這只是對承諾和延遲的jQuery實現的介紹。讓我們寫一些真實世界的例子。 (如果在node中執行,可以使用$ = require('jquery');導入jQuery)

一些jQuery示例

例如,這裡我們執行一個函數,當它完成時調用dfd.resolve()。類似於使用回調,但更具結構性和可重用性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$.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()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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”的方式:

1
2
3
4
5
6
7
8
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可以用於的另一個例子:看看這個

1
2
3
4
5
6
var interval = setInterval(function() {
if (App.value) {
clearInterval(interval);
// do things
}
}, 100);

這是一個評估條件的結構;如果條件為true,代碼將清除間隔並執行if內容。

這對於例如檢查當一個值不再是undefined時很有用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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不言自明。

請記住,基本類型按值傳遞,因此您不能這樣做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);

或者你不能這樣做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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中使用的一個功能是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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();
}
}

因此,我可以在客戶端代碼中這樣做:

1
2
3
4
$.when(App.DeferredHelper.arrayContainsElements('itemsController.content'))
.then(function() {
//do things
});

1
2
3
4
5
6
7
8
9
10
11
$.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調用返回,繼續處理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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()函數將它們處理,然後一旦完成處理所有項目,我可以做一些事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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