首页 > 代码库 > JavaScript Promise启示录

JavaScript Promise启示录

转自:http://www.csdn.net/article/2014-05-28/2819979-JavaScript-Promise

一篇非常不做的promise介绍,向原作者致敬;


<iframe src="http://hits.sinajs.cn/A1/weiboshare.html?url=http%3A%2F%2Fwww.csdn.net%2Farticle%2F2014-05-28%2F2819979-JavaScript-Promise&type=3&count=&appkey=&title=%E7%9B%AE%E5%89%8D%E9%AB%98%E7%BA%A7%E6%B5%8F%E8%A7%88%E5%99%A8%E5%A6%82Chrome%E3%80%81Firefox%E9%83%BD%E5%B7%B2%E7%BB%8F%E5%86%85%E7%BD%AE%E4%BA%86Promise%E5%AF%B9%E8%B1%A1%EF%BC%8C%E6%8F%90%E4%BE%9B%E6%9B%B4%E5%A4%9A%E7%9A%84%E6%93%8D%E4%BD%9C%E6%8E%A5%E5%8F%A3%EF%BC%8C%E5%A6%82%E6%AD%A4%E4%BC%98%E9%9B%85%E7%9A%84Promise%E5%85%B7%E5%A4%87%E5%93%AA%E4%BA%9B%E7%89%B9%E6%80%A7%E5%91%A2%EF%BC%9F%E4%BD%9C%E8%80%85TAT.dmyang%E5%B0%B1JavaScript%E4%B8%AD%E7%9A%84Promise%E8%A7%84%E8%8C%83%E7%BB%99%E5%87%BA%E4%BA%86%E4%B8%80%E4%BA%9B%E8%A7%81%E8%A7%A3%EF%BC%8C%E4%B8%80%E8%B5%B7%E6%9D%A5%E7%9C%8B%E4%B8%8B%E3%80%82&pic=&ralateUid=&language=zh_cn&rnd=1414397671289" frameborder="0" scrolling="no" width="22" height="16"></iframe>摘要:目前高级浏览器如Chrome、Firefox都已经内置了Promise对象,提供更多的操作接口,如此优雅的Promise具备哪些特性呢?作者TAT.dmyang就JavaScript中的Promise规范给出了一些见解,一起来看下。

【编者按】JavaScript是一种基于对象和事件驱动并具有相对安全性的客户端脚本语言。自推出后就大受开发者的青睐,基于JavaScript的开发工具也不计其数,开发者们可以灵活选择,轻松构建应用。原文作者TAT.dmyang就JavaScript中的Promise规范给出了一些见解,目前高级浏览器如Chrome、Firefox都已经内置了Promise对象,提供更多的操作接口,如此优雅的Promise具备哪些特性呢?且看下文:


一直以来,JavaScript处理异步都是以callback的方式,在前端开发领域callback机制几乎深入人心。在设计API的时候,不管是浏览器厂商还是SDK开发商亦或是各种类库的作者,基本上都已经遵循着callback的套路。

近几年随着JavaScript开发模式的逐渐成熟,CommonJS规范顺势而生,其中就包括提出了Promise规范,Promise完全改变了js异步编程的写法,让异步编程变得十分的易于理解。

在callback的模型里边,我们假设需要执行一个异步队列,代码看起来可能像这样:

 

[js] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. loadImg(‘a.jpg‘, function() {  
  2.     loadImg(‘b.jpg‘, function() {  
  3.         loadImg(‘c.jpg‘, function() {  
  4.             console.log(‘all done!‘);  
  5.         });  
  6.     });  
  7. });  

 

这也就是我们常说的回调金字塔,当异步的任务很多的时候,维护大量的callback将是一场灾难。当今Node.js大热,好像很多团队都要用它来做点东西以沾沾“洋气”,曾经跟一个运维的同学聊天,他们也是打算使用Node.js做一些事情,可是一想到js的层层回调就望而却步。

好,扯淡完毕,下面进入正题。

Promise可能大家都不陌生,因为Promise规范已经出来好一段时间了,同时Promise也已经纳入了ES6,而且高版本的chrome、firefox浏览器都已经原生实现了Promise,只不过和现如今流行的类Promise类库相比少些API。

所谓Promise,字面上可以理解为“承诺”,就是说A调用B,B返回一个“承诺”给A,然后A就可以在写计划的时候这么写:当B返回结果给我的时候,A执行方案S1,反之如果B因为什么原因没有给到A想要的结果,那么A执行应急方案S2,这样一来,所有的潜在风险都在A的可控范围之内了。

上面这句话,翻译成代码类似:

[js] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. var resB = B();  
  2. var runA = function() {  
  3.     resB.then(execS1, execS2);  
  4. };  
  5. runA();  

只看上面这行代码,好像看不出什么特别之处。但现实情况可能比这个复杂许多,A要完成一件事,可能要依赖不止B一个人的响应,可能需要同时向多个人询问,当收到所有的应答之后再执行下一步的方案。最终翻译成代码可能像这样:

[js] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. var resB = B();  
  2. var resC = C();  
  3. ...  
  4.   
  5. var runA = function() {  
  6.     reqB  
  7.         .then(resC, execS2)  
  8.         .then(resD, execS3)  
  9.         .then(resE, execS4)  
  10.         ...  
  11.         .then(execS1);  
  12. };  
  13.   
  14. runA();  

在这里,当每一个被询问者做出不符合预期的应答时都用了不同的处理机制。事实上,Promise规范没有要求这样做,你甚至可以不做任何的处理(即不传入then的第二个参数)或者统一处理。

好了,下面我们来认识下Promise/A+规范:

  • 一个promise可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)
  • 一个promise的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换
  • promise必须实现then方法(可以说,then就是promise的核心),而且then必须返回一个promise,同一个promise的then可以调用多次,并且回调的执行顺序跟它们被定义时的顺序一致
  • then方法接受两个参数,第一个参数是成功时的回调,在promise由“等待”态转换到“完成”态时调用,另一个是失败时的回调,在promise由“等待”态转换到“拒绝”态时调用。同时,then可以接受另一个promise传入,也接受一个“类then”的对象或方法,即thenable对象。

可以看到,Promise规范的内容并不算多,大家可以试着自己实现以下Promise。

以下是笔者自己在参考许多类Promise库之后简单实现的一个Promise,代码请移步promiseA。

简单分析下思路:

构造函数Promise接受一个函数resolver,可以理解为传入一个异步任务,resolver接受两个参数,一个是成功时的回调,一个是失败时的回调,这两参数和通过then传入的参数是对等的。

其次是then的实现,由于Promise要求then必须返回一个promise,所以在then调用的时候会新生成一个promise,挂在当前promise的_next上,同一个promise多次调用都只会返回之前生成的_next

由于then方法接受的两个参数都是可选的,而且类型也没限制,可以是函数,也可以是一个具体的值,还可以是另一个promise。下面是then的具体实现:

[js] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. Promise.prototype.then = function(resolve, reject) {  
  2.     var next = this._next || (this._next = Promise());  
  3.     var status = this.status;  
  4.     var x;  
  5.   
  6.     if(‘pending‘ === status) {  
  7.         isFn(resolve) && this._resolves.push(resolve);  
  8.         isFn(reject) && this._rejects.push(reject);  
  9.         return next;  
  10.     }  
  11.   
  12.     if(‘resolved‘ === status) {  
  13.         if(!isFn(resolve)) {  
  14.             next.resolve(resolve);  
  15.         } else {  
  16.             try {  
  17.                 x = resolve(this.value);  
  18.                 resolveX(next, x);  
  19.             } catch(e) {  
  20.                 this.reject(e);  
  21.             }  
  22.         }  
  23.         return next;  
  24.     }  
  25.   
  26.     if(‘rejected‘ === status) {  
  27.         if(!isFn(reject)) {  
  28.             next.reject(reject);  
  29.         } else {  
  30.             try {  
  31.                 x = reject(this.reason);  
  32.                 resolveX(next, x);  
  33.             } catch(e) {  
  34.                 this.reject(e);  
  35.             }  
  36.         }  
  37.         return next;  
  38.     }  
  39. };  

这里,then做了简化,其他promise类库的实现比这个要复杂得多,同时功能也更多,比如还有第三个参数——notify,表示promise当前的进度,这在设计文件上传等时很有用。对then的各种参数的处理是最复杂的部分,有兴趣的同学可以参看其他类Promise库的实现。

在then的基础上,应该还需要至少两个方法,分别是完成promise的状态从pending到resolved或rejected的转换,同时执行相应的回调队列,即resolve()reject()方法。

到此,一个简单的promise就设计完成了,下面简单实现下两个promise化的函数:

[js] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. function sleep(ms) {  
  2.     return function(v) {  
  3.         var p = Promise();  
  4.   
  5.         setTimeout(function() {  
  6.             p.resolve(v);  
  7.         });  
  8.   
  9.         return p;  
  10.     };  
  11. };  
  12.   
  13. function getImg(url) {  
  14.     var p = Promise();  
  15.     var img = new Image();  
  16.   
  17.     img.onload = function() {  
  18.         p.resolve(this);  
  19.     };  
  20.   
  21.     img.onerror = function(err) {  
  22.         p.reject(err);  
  23.     };  
  24.   
  25.     img.url = url;  
  26.   
  27.     return p;  
  28. };  

由于Promise构造函数接受一个异步任务作为参数,所以getImg还可以这样调用:

[js] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. function getImg(url) {  
  2.     return Promise(function(resolve, reject) {  
  3.         var img = new Image();  
  4.   
  5.         img.onload = function() {  
  6.             resolve(this);  
  7.         };  
  8.   
  9.         img.onerror = function(err) {  
  10.             reject(err);  
  11.         };  
  12.   
  13.         img.url = url;  
  14.     });  
  15. };  

接下来(见证奇迹的时刻),假设有一个BT的需求要这么实现:异步获取一个json配置,解析json数据拿到里边的图片,然后按顺序队列加载图片,没张图片加载时给个loading效果

[js] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. function addImg(img) {  
  2.     $(‘#list‘).find(‘> li:last-child‘).html(‘‘).append(img);  
  3. };  
  4.   
  5. function prepend() {  
  6.     $(‘<li>‘)  
  7.         .html(‘loading...‘)  
  8.         .appendTo($(‘#list‘));  
  9. };  
  10.   
  11. function run() {  
  12.     $(‘#done‘).hide();  
  13.     getData(‘map.json‘)  
  14.         .then(function(data) {  
  15.             $(‘h4‘).html(data.name);  
  16.   
  17.             return data.list.reduce(function(promise, item) {  
  18.                 return promise  
  19.                     .then(prepend)  
  20.                     .then(sleep(1000))  
  21.                     .then(function() {  
  22.                         return getImg(item.url);  
  23.                     })  
  24.                     .then(addImg);  
  25.             }, Promise.resolve());  
  26.         })  
  27.         .then(sleep(300))  
  28.         .then(function() {  
  29.             $(‘#done‘).show();  
  30.         });  
  31. };  
  32.   
  33. $(‘#run‘).on(‘click‘, run);  

这里的sleep只是为了看效果加的,可猛击查看demo!当然,Node.js的例子可查看这里。

在这里,Promise.resolve(v)静态方法只是简单返回一个以v为肯定结果的promise,v可不传入,也可以是一个函数或者是一个包含then方法的对象或函数(即thenable)。

类似的静态方法还有Promise.cast(promise),生成一个以promise为肯定结果的promise;

Promise.reject(reason),生成一个以reason为否定结果的promise。

我们实际的使用场景可能很复杂,往往需要多个异步的任务穿插执行,并行或者串行同在。这时候,可以对Promise进行各种扩展,比如实现Promise.all(),接受promises队列并等待他们完成再继续,再比如Promise.any(),promises队列中有任何一个处于完成态时即触发下一步操作。

标准的Promise

可参考html5rocks的这篇文章JavaScript Promises,目前高级浏览器如Chrome、Firefox都已经内置了Promise对象,提供更多的操作接口,比如Promise.all(),支持传入一个promises数组,当所有promises都完成时执行then,还有就是更加友好强大的异常捕获,应对日常的异步编程,应该足够了。

第三方库的Promise

现今流行的各大js库,几乎都不同程度的实现了Promise,如dojo,jQuery、Zepto、when.js、Q等,只是暴露出来的大都是Deferred对象,以jQuery(Zepto类似)为例,实现上面的getImg()

[js] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. function getImg(url) {  
  2.     var def = $.Deferred();  
  3.     var img = new Image();  
  4.   
  5.     img.onload = function() {  
  6.         def.resolve(this);  
  7.     };  
  8.   
  9.     img.onerror = function(err) {  
  10.         def.reject(err);  
  11.     };  
  12.   
  13.     img.src = url;  
  14.   
  15.     return def.promise();  
  16. };  

当然,jQuery中,很多的操作都返回的是Deferred或promise,如animateajax

[js] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. // animate  
  2. $(‘.box‘)  
  3.     .animate({‘opacity‘: 0}, 1000)  
  4.     .promise()  
  5.     .then(function() {  
  6.         console.log(‘done‘);  
  7.     });  
  8.   
  9. // ajax  
  10. $.ajax(options).then(success, fail);  
  11. $.ajax(options).done(success).fail(fail);  
  12.   
  13. // ajax queue  
  14. $.when($.ajax(options1), $.ajax(options2))  
  15.     .then(function() {  
  16.         console.log(‘all done.‘);  
  17.     }, function() {  
  18.         console.error(‘There something wrong.‘);  
  19.     });  

jQuery还实现了done()fail()方法,其实都是then方法的shortcut。

处理promises队列,jQuery实现的是$.when()方法,用法和Promise.all()类似。

其他类库,这里值得一提的是when.js,本身代码不多,完整实现Promise,同时支持browser和Node.js,而且提供更加丰富的API,是个不错的选择。这里限于篇幅,不再展开。

尾声

我们看到,不管Promise实现怎么复杂,但是它的用法却很简单,组织的代码很清晰,从此不用再受callback的折磨了。

最后,Promise是如此的优雅!但Promise也只是解决了回调的深层嵌套的问题,真正简化JavaScript异步编程的还是Generator,在Node.js端,建议考虑Generator。

 

参考文献

  • JavaScript Promises
  • JavaScript Promises(中文)
  • when.js
  • Asynch JS: The Power Of $.Deferred
  • jQuery: $.Deferred()
原文出自:Alloyteam

JavaScript Promise启示录