您好,欢迎访问一九零五行业门户网

JavaScript Promise启示录_javascript技巧

本篇,主要普及promise的用法。
一直以来,javascript处理异步都是以callback的方式,在前端开发领域callback机制几乎深入人心。在设计api的时候,不管是浏览器厂商还是sdk开发商亦或是各种类库的作者,基本上都已经遵循着callback的套路。
近几年随着javascript开发模式的逐渐成熟,commonjs规范顺势而生,其中就包括提出了promise规范,promise完全改变了js异步编程的写法,让异步编程变得十分的易于理解。
在callback的模型里边,我们假设需要执行一个异步队列,代码看起来可能像这样:
loadimg('a.jpg', function() { loadimg('b.jpg', function() { loadimg('c.jpg', function() { console.log('all done!'); }); });});
这也就是我们常说的回调金字塔,当异步的任务很多的时候,维护大量的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的可控范围之内了。
上面这句话,翻译成代码类似:
var resb = b();var runa = function() { resb.then(execs1, execs2);};runa();
只看上面这行代码,好像看不出什么特别之处。但现实情况可能比这个复杂许多,a要完成一件事,可能要依赖不止b一个人的响应,可能需要同时向多个人询问,当收到所有的应答之后再执行下一步的方案。最终翻译成代码可能像这样:
var resb = b();var resc = c();...var runa = function() { reqb .then(resc, execs2) .then(resd, execs3) .then(rese, execs4) ... .then(execs1);};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的具体实现:
promise.prototype.then = function(resolve, reject) { var next = this._next || (this._next = promise()); var status = this.status; var x; if('pending' === status) { isfn(resolve) && this._resolves.push(resolve); isfn(reject) && this._rejects.push(reject); return next; } if('resolved' === status) { if(!isfn(resolve)) { next.resolve(resolve); } else { try { x = resolve(this.value); resolvex(next, x); } catch(e) { this.reject(e); } } return next; } if('rejected' === status) { if(!isfn(reject)) { next.reject(reject); } else { try { x = reject(this.reason); resolvex(next, x); } catch(e) { this.reject(e); } } return next; }};
这里,then做了简化,其他promise类库的实现比这个要复杂得多,同时功能也更多,比如还有第三个参数——notify,表示promise当前的进度,这在设计文件上传等时很有用。对then的各种参数的处理是最复杂的部分,有兴趣的同学可以参看其他类promise库的实现。
在then的基础上,应该还需要至少两个方法,分别是完成promise的状态从pending到resolved或rejected的转换,同时执行相应的回调队列,即resolve()和reject()方法。
到此,一个简单的promise就设计完成了,下面简单实现下两个promise化的函数:
function sleep(ms) { return function(v) { var p = promise(); settimeout(function() { p.resolve(v); }, ms); return p; };};function getimg(url) { var p = promise(); var img = new image(); img.onload = function() { p.resolve(this); }; img.onerror = function(err) { p.reject(err); }; img.url = url; return p;};
由于promise构造函数接受一个异步任务作为参数,所以getimg还可以这样调用:
function getimg(url) { return promise(function(resolve, reject) { var img = new image(); img.onload = function() { resolve(this); }; img.onerror = function(err) { reject(err); }; img.url = url; });};
接下来(见证奇迹的时刻),假设有一个bt的需求要这么实现:异步获取一个json配置,解析json数据拿到里边的图片,然后按顺序队列加载图片,没张图片加载时给个loading效果
function addimg(img) { $('#list').find('> li:last-child').html('').append(img);};function prepend() { $('') .html('loading...') .appendto($('#list'));};function run() { $('#done').hide(); getdata('map.json') .then(function(data) { $('h4').html(data.name); return data.list.reduce(function(promise, item) { return promise .then(prepend) .then(sleep(1000)) .then(function() { return getimg(item.url); }) .then(addimg); }, promise.resolve()); }) .then(sleep(300)) .then(function() { $('#done').show(); });};$('#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():
function getimg(url) { var def = $.deferred(); var img = new image(); img.onload = function() { def.resolve(this); }; img.onerror = function(err) { def.reject(err); }; img.src = url; return def.promise();};
当然,jquery中,很多的操作都返回的是deferred或promise,如animate、ajax:
// animate$('.box') .animate({'opacity': 0}, 1000) .promise() .then(function() { console.log('done'); });// ajax$.ajax(options).then(success, fail);$.ajax(options).done(success).fail(fail);// ajax queue$.when($.ajax(options1), $.ajax(options2)) .then(function() { console.log('all done.'); }, function() { console.error('there something wrong.'); });
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。
下一篇,研究下generator。
github原文: https://github.com/chemdemo/chemdemo.github.io/issues/6
其它类似信息

推荐信息