用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本身没有什么特别的功能,但是它背后代表的编程思路是很有价值的。

w3ctech微信

扫码关注w3ctech微信公众号

共收到12条回复

  • 一下子清晰了太多!

    回复此楼
  • 题目错了,哥

    回复此楼
  • 已经改正

    回复此楼
  • 现代浏览器中有个 Promise,是指这个吗? 还是类似 async,q之类的解决方案。 我并没有仔细看文章哈,抱歉楼主。

    回复此楼
  • 是现代浏览器的Promise

    回复此楼
  • Promise + generator 一起用才爽,co都用Promise重写了

    yield 一个 Promise 和 C# 里面 await 一个 Task<TResult> 简直不能更像

    回复此楼
  • 这头像也太帅了,,哈哈,,winter老师

    回复此楼
  • 写的很好,就是没学会,哈哈

    回复此楼
  • @winter 请问哈,如果是非现代浏览器的话,有什么合适的替代方案吗?

    回复此楼
  • Promise.race(wait(5000), waitFor(btn, click))

    race 接收数组

    回复此楼
  • 写的很清晰,赞一个

    回复此楼
  • 真的很不错,很实用,感谢!

    回复此楼

AltStyle によって変換されたページ (->オリジナル) /