用Promise组织程序
一、Promise基本用法
很多文章介绍Promise给的例子是这样的:
new Promise(function(resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open('POST', location.href, true); xhr.send(null); xhr.addEventListener('readystatechange', function(e){ if(xhr.readyState === 4) { if(xhr.status === 200) { resolve(xhr.responseText); } else { reject(xhr); } } }) }).then(function(txt){ console.log(); })
一定会有小朋友好奇,说尼玛,这不是比回调还恶心?
这种写法的确是能跑得起来啦……不过,按照Promise的设计初衷,我们编程需要使用的概念并非"Promise对象",而是promise函数,凡是以Promise作为返回值的函数,称为promise函数(我暂且取了这个名字)。所以应该是这样的:
function doSth() { return new Promise(function(resolve, reject) { //做点什么异步的事情 //结束的时候调用 resolve,比如: setTimeout(function(){ resolve(); //这里才是真的返回 },1000) }) }
如果你不喜欢这样的写法,还可以使用defer风格的promise
function doSth2() { var defer = Promise.defer(); //做点什么异步的事情 //结束的时候调用 defer.resolve,比如: setTimeout(function(){ defer.resolve(); //这里才是真的返回 },1000) return defer.promise; }
总之两种是没什么区别啦。
然后你就可以这么干:
doSth().then(doSth2).then(doSth);
这样看起来就顺眼多了吧。
其实说简单点,promise最大的意义在于把嵌套的回调变成了链式调用(详见第三节,顺序执行),比如以下
//回调风格 loadImage(img1,function(){ loadImage(img2,function(){ loadImage(img3,function(){ }); }); }); //promise风格 Promise.resolve().then(function(){ return loadImage(img1); }).then(function(){ return loadImage(img2); }).then(function(){ return loadImage(img3); });
后者嵌套关系更少,在多数人眼中会更易于维护一些。
二、Promise风格的API
在去完cssconf回杭州的火车上,我顺手把一些常见的JS和API写成了promise方式:
function get(uri){ return http(uri, 'GET', null); } function post(uri,data){ if(typeof data === 'object' && !(data instanceof String || (FormData && data instanceof FormData))) { var params = []; for(var p in data) { if(data[p] instanceof Array) { for(var i = 0; i < data[p].length; i++) { params.push(encodeURIComponent(p) + '[]=' + encodeURIComponent(data[p][i])); } } else { params.push(encodeURIComponent(p) + '=' + encodeURIComponent(data[p])); } } data = params.join('&'); } return http(uri, 'POST', data || null, { "Content-type":"application/x-www-form-urlencoded" }); } function http(uri,method,data,headers){ return new Promise(function(resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open(method,uri,true); if(headers) { for(var p in headers) { xhr.setRequestHeader(p, headers[p]); } } xhr.addEventListener('readystatechange',function(e){ if(xhr.readyState === 4) { if(String(xhr.status).match(/^2\d\d$/)) { resolve(xhr.responseText); } else { reject(xhr); } } }); xhr.send(data); }) } function wait(duration){ return new Promise(function(resolve, reject) { setTimeout(resolve,duration); }) } function waitFor(element,event,useCapture){ return new Promise(function(resolve, reject) { element.addEventListener(event,function listener(event){ resolve(event) this.removeEventListener(event, listener, useCapture); },useCapture) }) } function loadImage(src) { return new Promise(function(resolve, reject) { var image = new Image; image.addEventListener('load',function listener() { resolve(image); this.removeEventListener('load', listener, useCapture); }); image.src = src; image.addEventListener('error',reject); }) } function runScript(src) { return new Promise(function(resolve, reject) { var script = document.createElement('script'); script.src = src; script.addEventListener('load',resolve); script.addEventListener('error',reject); (document.getElementsByTagName('head')[0] || document.body || document.documentElement).appendChild(script); }) } function domReady() { return new Promise(function(resolve, reject) { if(document.readyState === 'complete') { resolve(); } else { document.addEventListener('DOMContentLoaded',resolve); } }) }
看到了吗,Promise风格API跟回调风格的API不同,它的参数跟同步的API是一致的,但是它的返回值是个Promise对象,要想得到真正的结果,需要在then的回调里面拿到。
值得一提的是,下面这样的写法:
waitFor(document.documentElement,'click').then(function(){ console.log('document clicked!') })
这样的事件响应思路上是比较新颖的,不同于事件机制,它的事件处理代码仅会执行一次。
通过这些函数的组合,我们可以更优雅地组织异步代码,请继续往下看。
三、使用Promise组织异步代码
函数调用/并行
Promise.all跟then的配合,可以视为调用部分参数为Promise提供的函数。譬如,我们现在有一个接受三个参数的函数
function print(a, b, c) { console.log(a + b + c); }
现在我们调用print函数,其中a和b是需要异步获取的:
var c = 10; print(geta(), getb(), 10); //这是同步的写法 Promise.all([geta(), getb(), 10]).then(print); //这是 primise 的异步写法
竞争
如果说Primise.all是promise对象之间的“与”关系,那么Promise.race就是promise对象之间的“或”关系。
比如,我要实现“点击按钮或者5秒钟之后执行”
var btn = document.getElementsByTagName('button'); Promise.race(wait(5000), waitFor(btn, click)).then(function(){ console.log('run!') })
异常处理
异常处理一直是回调的难题,而promise提供了非常方便的catch方法:
在一次promise调用中,任何的环节发生reject,都可以在最终的catch中捕获到
Promise.resolve().then(function(){ return loadImage(img1); }).then(function(){ return loadImage(img2); }).then(function(){ return loadImage(img3); }).catch(function(err){ //错误处理 })
这非常类似于JS的try catch功能。
复杂流程
接下来,我们来看比较复杂的情况。
promise有一种非常重要的特性:then的参数,理论上应该是一个promise函数,而如果你传递的是普通函数,那么默认会把它当做已经resolve了的promise函数。
这样的特性让我们非常容易把promise风格的函数跟已有代码结合起来。
为了方便传参数,我们编写一个currying函数,这是函数式编程里面的基本特性,在这里跟promise非常搭,所以就实现一下:
function currying(){ var f = arguments[0]; var args = Array.prototype.slice.call(arguments,1); return function(){ args.push.apply(args,arguments); return f.apply(this,args); } }
currying会给某个函数"固化"几个参数,并且返回接受剩余参数的函数。比如之前的函数,可以这么玩:
var print2 = currying(print,11); print2(2, 3); //得到 11 + 2 + 3 的结果,16 var wait1s = currying(wait,1000); wait1s().then(function(){ console.log('after 1s!'); })
有了currying,我们就可以愉快地来玩链式调用了,比如以下代码:
Promise.race([ domReady().then(currying(wait,5000)), waitFor(btn, click)]) .then(currying(runScript,'a.js')) .then(function(){ console.log('loaded'); return Promise.resolve(); });
表示“domReady发生5秒或者点击按钮后,加载脚本并执行,完毕后打印loaded”。
四、总结
promise作为一个新的API,是几乎完全可polyfill的,这在JS发展中这是很少见的情况,它的API本身没有什么特别的功能,但是它背后代表的编程思路是很有价值的。
来自:http://www.w3ctech.com/topic/721