Deferred 和 Promises 实例详解

最近在网上找了很多关于jquery的Deferred介绍的文章,唯独觉得这篇外文博文介绍的比较详细,现翻译出来:

原文:Deferreds and Promises in JavaScript(http://flaviocopes.com/deferred-and-promises-in-javascript/)

Promises是一种实现javascript异步调用的新方式,它对于结构化你的javascript代码非常有帮助。

一个Promise代表了一个事件对象,在这个对象的整个生命周期中,它从刚开始调用时的挂起状态,到完成调用的解决或拒绝状态,或者它一直处于挂起状态而永远也不被解决。

它是javascript事件的一种新方式,我认为它有助于改善代码的可读性而且你不会觉得很怪异。到目前为止,有两种不同实现Promise的方式,一种是按照Promises/A Spec实现的各种库,另一种就是jQuery。

由于jQuery类库比较常用,因此接下来我将主要用jQuery的实现来讨论它,如果你不想使用其他的库,你也可以使用它。

一、 jQuery promise 介绍:

让我首先介绍下Deferred的概念,首先,Deferred是一个Promise,另外你可以触发一个Deferred(resolve或reject),相反,Promise对象你只能给它添加回调函数,它被其他对象触发。

一个Promise对象,就是一个只读的Deferred对象。

 

一个简单的例子是下面这个函数:

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

你现在可以添加done()和fail()来处理回调。

这只是一个调用的例子,在jQuery1.8中,动态的promises变成了实时处理,并且附带了一个progress的回调函数。

另一个promise的例子是ajax调用:

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

一个deferred对象是你创建的,并且设置了回调函数,并解决的promise,如:

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

deferred对象的状态可以使用resolve() 和 reject()触发,一旦一个deferred对象的状态变成了最终的状态(resolved/rejected),它就再也不能被改变了如:

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

我们可以添加一下这些回调到一个promise对象上:

.done()  //当promise执行成功时被调用
.fail()  //当promise失败是被调用
.always()  //不管是成功和失败状态,最终都会被调用

这些回调函数,可以使用then() , 一起调用:

promise.then(doneFunc , failFunc , alwaysFunc);

上面只是一个简单的介绍jquery对promises和deferred的实现,下面会用几个实例深入讲解:

二、一些jQuery的Deferred实例

比如,下面执行一个函数,当它执行完后调用dfd.resolve(),就像执行了一个回调函数,但代码更加结构化和复用性。

 1 $.when(execution()).then(executionDone); 
 2  
 3 function execution(data) {
 4   var dfd = $.Deferred();
 5   console.log('start execution');
 6  
 7   //in the real world, this would probably make an AJAX call.
 8   setTimeout(function() { dfd.resolve() }, 2000);    
 9  
10   return dfd.promise();
11 }
12  
13 function executionDone(){
14   console.log('execution ended');
15 }

实例二:

下面是一个被处理的数组元素,一旦所有的元素都处理完,就调用另外一个函数,让我们来看看使用Deferred的真正好处,$.when.apply()方法用于将所有的dfd.resolve()组织在一起。

 1 var data = [1,2,3,4]; // the ids coming back from serviceA
 2 var processItemsDeferred = [];
 3  
 4 for(var i = 0; i < data.length; i++){
 5   processItemsDeferred.push(processItem(data[i]));
 6 }
 7  
 8 $.when.apply($, processItemsDeferred).then(everythingDone); 
 9  
10 function processItem(data) {
11   var dfd = $.Deferred();
12   console.log('called processItem');
13  
14   //in the real world, this would probably make an AJAX call.
15   setTimeout(function() { dfd.resolve() }, 2000);    
16  
17   return dfd.promise();
18 }
19  
20 function everythingDone(){
21   console.log('processed all items');
22 }

实例三:

一个稍微更复杂点的例子,在这里数组里的元素从其他地方获取,使用var fetchItemIdsDeferred = fetchItemIds(data) and fetchItemIdsDeferred.done()

 1 var data = []; // the ids coming back from serviceA
 2 var fetchItemIdsDeferred = fetchItemIds(data); // has to add the ids to data
 3  
 4 function fetchItemIds(data){
 5   var dfd = $.Deferred();
 6   console.log('calling fetchItemIds');
 7  
 8   data.push(1);
 9   data.push(2);
10   data.push(3);
11   data.push(4);        
12  
13   setTimeout(function() { dfd.resolve() }, 1000);
14   return dfd.promise();
15 }
16  
17 fetchItemIdsDeferred.done(function() { // if fetchItemIds successful...
18   var processItemsDeferred = [];
19  
20   for(var i = 0; i < data.length; i++){
21     processItemsDeferred.push(processItem(data[i]));
22   }
23  
24   $.when.apply($, processItemsDeferred).then(everythingDone); 
25 });
26   
27  
28 function processItem(data) {
29   var dfd = $.Deferred();
30   console.log('called processItem');
31  
32   //in the real world, this would probably make an AJAX call.
33   setTimeout(function() { dfd.resolve() }, 2000);    
34  
35   return dfd.promise();
36 }
37  
38 function everythingDone(){
39   console.log('processed all items');
40 }

实例四:

下面是两个我实例,在不用Defrred的情况下,这个程序周期性做同一件事,并等待处理结束后执行其他事情。

常用的做法如下:

1 var allProcessed = false;
2 var countProcessed = 0;
3 for (var i = 0, len = theArray.length; i < len; i++) {
4   (function(i) {
5     // do things with i
6         if (++countProcessed === len) allProcessed = true;
7   })(i);
8 }

另外一个例子:

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

这是一种结构,代码周期性的判断一个条件是否为true,如果是,则clear interval,并且执行里面的代码。

下面是使用Deferred处理上述情况:

 1 var DeferredHelper = {
 2   objectVariableIsSet: function(object, variableName) {
 3     var dfd = $.Deferred();
 4  
 5     var interval = setInterval(function() {
 6       if (object[variableName] !== undefined) {
 7         clearInterval(interval);
 8         console.log('objectVariableIsSet');
 9         dfd.resolve()
10       }
11     }, 10);
12  
13     return dfd.promise();
14   },
15  
16   arrayContainsElements: function(array) {
17     var dfd = $.Deferred();
18  
19     var interval = setInterval(function() {
20       if (array.length > 0) {
21         clearInterval(interval);
22         console.log('arrayContainsElements');
23         dfd.resolve()
24       }
25     }, 10);
26  
27     return dfd.promise();
28   }
29 }
30  
31 var executeThis = function() {
32   console.log('ok!');
33 }
34  
35 var object = {};
36 object.var = undefined;
37 var array = [];
38  
39 $.when(DeferredHelper.arrayContainsElements(array)).then(executeThis);
40 $.when(DeferredHelper.objectVariableIsSet(object, 'var')).then(executeThis);
41  
42 setTimeout(function() {
43   object.var = 2;
44   array.push(2);
45   array.push(3);
46 }, 2000);

上面的例子,实际上集中了以上三个的功能,我首先创建了一个DeferredHelper对象并且他的另外两个方法arrayContainsElements和objectVariableIsSet功能是自解释的。

考虑到初始值传进来不能做

 1 var integerIsGreaterThanZero = function(integer) {
 2   var dfd = $.Deferred();
 3  
 4   var interval = setInterval(function() {
 5     if (integer > 0) {
 6       clearInterval(interval);
 7       dfd.resolve()
 8     }
 9   }, 10);
10     
11   return dfd.promise();
12 };
13  
14 var variable = 0;
15  
16 $.when(integerIsGreaterThanZero(variable)).then(executeThis);

也不能这样做:

 1 var object = null;
 2  
 3 var variableIsSet = function(object) {
 4   var dfd = $.Deferred();
 5  
 6   var interval = setInterval(function() {
 7     if (object !== undefined) {
 8       clearInterval(interval);
 9       console.log('variableIsSet');
10       dfd.resolve()
11     }
12   }, 10);
13  
14   return dfd.promise();
15 };
16  
17 $.when(variableIsSet(object)).then(executeThis);
18  
19 setTimeout(function() {
20   object = {};
21 }, 2000);

because when doing object = {}, the object reference is changed, and as Javascript actually references variables by copy-reference, the reference of the object variable inside the variableIsSet function is not the same as the outer object variable.

一个在ember.js中的例子:

 1 App.DeferredHelper = {
 2  
 3   /**
 4    * Check if an array has elements on the App global object if object 
 5    * is not set.
 6    * If object is set, check on that object.
 7    */
 8   arrayContainsElements: function(arrayName, object) {
 9     var dfd = $.Deferred();
10     if (!object) object = App;
11  
12     var interval = setInterval(function() {
13       if (object.get(arrayName).length > 0) {
14         clearInterval(interval);
15         dfd.resolve()
16       }
17     }, 50);
18  
19     return dfd.promise();
20   },
21  
22   /**
23    * Check if a variable is set on the App global object if object 
24    * is not set.
25    * If object is set, check on that object.
26    */
27   variableIsSet: function(variableName, object) {
28     var dfd = $.Deferred();
29     if (!object) object = App;
30  
31     var interval = setInterval(function() {
32       if (object.get(variableName) !== undefined) {
33         clearInterval(interval);
34         dfd.resolve()
35       }
36     }, 50);
37  
38     return dfd.promise();
39   }
40 }

因此我在客户端可以使用:

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

 1 $.when(App.DeferredHelper.variableIsSet('aVariable'))
 2  .then(function() {
 3   //do things
 4 });
 5     
 6 //&
 7  
 8 $.when(App.DeferredHelper.variableIsSet('aVariable', anObject))
 9  .then(function() {
10   //do things
11 });   

所有这些都使用了jQuery的deferreds 实现。

其他的资源:

http://robdodson.me/blog/2012/06/03/make-your-own-jquery-deferreds-and-promises/
http://msdn.microsoft.com/en-us/magazine/gg723713.aspx

也许你并不想使用jQuery的deferred实现,或者你使用其他的库但没有实现这个功能,你可以使用其他专有的库如Qrsvp.jswhen.js.

让我们看看是哟个when.js的例子:

例如:我有一个元素的ID,我想在终点调用API获取到更多详细信息,一旦ajax调用返回,继续处理:

 1 function processItem(item) {
 2   var deferred = when.defer();
 3  
 4   var request = $.ajax({
 5     url: '/api/itemDetails',
 6     type: 'GET'
 7     data: {
 8       item: item
 9     }
10   });
11  
12   request.done(function(response) {
13     deferred.resolve(JSON.parse(response)); 
14   });
15    
16   request.fail(function(response) {
17     deferred.reject('error');
18   });
19    
20   return deferred.promise;
21 }
22  
23 var item = {
24   id: 1
25 }
26  
27 processItem(item).then(
28   function gotIt(itemDetail) {
29     console.log(itemDetail);
30   },
31   function doh(err) {
32     console.error(err);
33   }
34 );

我从服务器获取了一些ID值,使用函数processItem()处理它们,然后一旦处理了所有这些元素,我就做其他事:

 1 function processItems(anArray) {
 2   var deferreds = [];
 3  
 4   for (var i = 0, len = anArray.length; i < len; i++) {
 5     deferreds.push(processItem(anArray[i].id));
 6   }
 7  
 8   return when.all(deferreds);
 9 }
10  
11 var anArray = [1, 2, 3, 4];
12  
13 processItems(anArray).then(
14   function gotEm(itemsArray) {
15     console.log(itemsArray);
16   },
17   function doh(err) {
18     console.error(err);
19   }
20 );

when.js库提供了一些工具方法比如when.any()和when.some(),这些让deferred的回调函数运行当1.有一个promise被solved,2.至少有一个特定的promise已经返回了。

posted @ 2013-09-13 14:59  嘻哈披头士  阅读(254)  评论(0)    收藏  举报