这篇有点长,不过干货挺多,既分析promise的原理,也包含一些最佳实践,亮点在最后:)

还记得上一节讲回调函数的时候,第一件事就提到了异步函数不能用return返回值,其原因就是在return语句执行的时候异步代码还没有执行完毕,所以return的值不是期望的运算结果。

Promise却恰恰要回过头来重新利用这个return语句,只不过不是返回最终运算值,而是返回一个对象,promise对象,用它来帮你进行异步流程管理。

先举个例子帮助理解。Promise对象可以想象成是工厂生产线上的一个工人,一条生产线由若干个工人组成,每个工人分工明确,自己做完了把产品传递给下一个工人继续他的工作,以此类推到最后就完成一个成品。这条生产线的组织机制就相当于Promise的机制,每个工人的工作相当于一个异步函数。后面会继续拿promise和这个例子进行类比。

Promise风格异步函数的基本写法:

如果用setTimeout来模拟你要进行的异步操作,以下是让异步函数返回promise的基本写法。调用Promise构造函数,生成一个promise对象,然后return它。把你的代码包裹在匿名函数function(resolve, reject){ … } 里面,作为参数传给Promise构造函数。resolve和reject是promise机制内部已经定义好的函数,传给你用来改变promise对象的状态。在你的异步代码结束的时候调用resolve来表示异步操作成功,并且把结果传给resolve作为参数,这样它可以传给下一个异步操作。

function asyncFn1() {
var promise = new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('asyncFn1 is done');
resolve('asyncFn1 value');
}, 1000);
}); return promise;
}

在promise机制当中,resolve被调用后会把promise的状态变成’resolved’。 如果reject被调用,则会把promise的状态变成’rejected’,表示异步操作失败。所以在上面的例子中如果你有一些逻辑判断,可以在失败的时候调用reject:

//伪代码
function asyncFn1() {
var promise = new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('asyncFn1 is done');
if(success) {
resolve('asyncFn1 value');
} else {
reject('error info');
}
}, 1000);
}); return promise;
}

then()方法:

既然promise的用来做流程管理的,那肯定是多个异步函数要按某种顺序执行,而每个都要return promise对象。怎样把它们串起来呢?答案是调用promise对象最重要的方法promsie.then(),从它的字面意思就可以看出它的作用。而且then()方法也返回一个新的promise对象,注意是新的promise对象,而不是返回之前那个。

假如有三个异步函数:

function asyncFn1() {
var promise = new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('asyncFn1 is done');
resolve('asyncFn1 value');
}, 1000);
});
return promise;
} function asyncFn2(arg) {
var promise = new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('asyncFn2 is done');
resolve(arg + ' asyncFn2 value');
}, 1000);
});
return promise;
} function asyncFn3(arg) {
var promise = new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('asyncFn3 is done');
resolve(arg + ' asyncFn3 value');
}, 1000);
});
return promise;
}

可以用then方法这样顺序来组织它们:

var p1 = asyncFn1(),
p2 = p1.then(asyncFn2),
p3 = p2.then(asyncFn3); p3.then(function(arg) {
console.log(arg);
});

这样组织起来后,就会按照顺序一个一个执行:asyncFn1执行完成后p1变成resolved状态并调用asyncFn2,asyncFn2运行完后p2变成resolved状态并且调用asyncFn3,asyncFn3执行完成后p3编程resolved状态并调用匿名函数打印输出结果。这个过程中,如果任何一个promise被变成’rejected’,后续所有promise马上跟着变成rejected,而不会继续执行他们所登记的异步函数。

上面代码可以更加简化成这样,看起来更清爽,用飘柔的感觉有没有:

asyncFn1()
.then(asyncFn2)
.then(asyncFn3)
.then(function(arg) {
console.log(arg);
});

怎么样,比上一节讲的回调嵌套代码漂亮太多啦,多苗条。

现在跟工厂生产线的例子进行类比一下加深理解。你猜上面这段飘柔代码在工厂生产线例子中相当于什么?你一定会说,你不是上面说了嘛,相当于一条顺序执行的生产线。错!!! 它相当于---------生产计划,或者生产图纸。怕了没?没错就是相当于生产计划,里面登记了每个工人的任务和他们的工作顺序。如果把它当成生产线,就会误以为asyncFn1()运行完了再调用then,当asyncFn2运行完了再调用下一个then,当asyncFn3运行完了再调用第三个then,这样会造成是由then来调用这些异步函数的错觉。实际上then的作用仅仅是登记当每个promise变成resolved状态时要调用的下一个函数,仅仅是登记,而不是实际上调用它们,实际调用是发生在promise变成resolved的时候。(then可以用来登记生产计划的原因是它其实是个同步方法,所以这段飘柔代码噌得一下就执行完了,计划就出来了,而不是跟着那些asyncFn函数们一个等一个的执行)。搞清楚这个对于新手来说非常重要,它可以让你更好的来组织你的异步流程。后面会详细说。另外,工作计划产生后,生产也同时开始了,即asyncFn函数们也开始执行了,按登记的顺序。

catch()方法:

上面例子中then方法都是只接受一个异步函数作为参数,实际上then方法可以接受两个函数作为参数。第一个函数是Promise对象的状态变为Resolved时调用,第二个回调函数是Promise对象 的状态变为Rejected时调用。其中,第二个函数是可选的,大部分情况下不需要提供。但是一种情况除外就是当你的异步流程结束的时候需要用第二个函数来捕获异常。即:

asyncFn1()
.then(asyncFn2)
.then(asyncFn3)
.then(null, function(error) {
console.log(error);
});

最后一步的异常捕获通常会换一种写法:

asyncFn1()
.then(asyncFn2)
.then(asyncFn3)
.catch(function(error) {
console.log(error);
});

catch()是then()用来捕获异常时的别名或语法糖。它可以捕获前面任何promise对象变成rejected的状态后,所传递下来的错误信息。如果不使用catch()方法,Promise对象抛出的错误就会石沉大海,让你无法调试。

嵌套promise

Promise机制本身是为了解决回调嵌套的,但有意思的是promise本身也可以嵌套,示例如下:

//伪代码
fn1()
.then(fn2)
.then(function(result) {
return fn3(result)
.then(fn31)
.then(fn32)
.then(fn33);
})
.then(fn4)
.catch(function(err) {
console.log(err);
});

你怎么看?我个人观点,任何事情都没有绝对的对和错,好和不好,就是个度的问题。

Promise.all()方法:

上一节在回调风格的异步中,最后留了一个思考题,怎样在循环里面调用异步函数?现在揭晓答案。

var fs = require('fs');

function foo(dir, callback) {
fs.readdir(dir, function(err, files) {
var text = '',
counter = files.length;
for(var i=0, j=files.length; i<j; ++i) {
void function(ii) {
fs.readFile(files[ii], 'utf8', function(err, data) {
text += data;
--counter;
if(counter===0) {
callback(text);
}
});
} (i);
}
});
} foo('./', function(data) {
console.log(data);
});

上面代码foo函数读取当前目录下所有文件然后合并到一起,由callback把内容传出来。调用callback的时机也很清楚了,关键就是设个计数器(counter),必须当所有readFile回调都完成后再调用callback。顺便提一下循环调用异步的时候循环本身必须使用一个匿名函数包裹,为什么?呵呵新手绕不过的坑,答案自行寻找。后面有时间再写文探讨一些javascript的坑坑吧。

怎样循环回调风格的异步函数现在清楚了,那么问题来了,怎样循环promise风格的函数呢?

var fs = require('fs');

//把fs.readdir()改造为promise风格
function readdirP(dir) {
return newPromise(function(resolve, reject) {
fs.readdir(dir, function(err, files) {
if(err) {
reject(err);
} else {
resolve(files);
}
});
});
} //把fs.readFile()改造为promise风格
function readFileP(file) {
return new Promise(function(resolve, reject) {
fs.readFile(file, 'utf8', function(err, data) {
if(err) {
reject(err);
} else {
resolve(data);
}
});
});
} function foo(dir) {
return new Promise(function(resolve, reject) {
var text = '';
readdirP(dir).then(function(files) {
return new Promise(function(resolve, reject) {
var counter = files.length;
console.log(counter);
for(var i=0, j=files.length; i<j; ++i) {
void function(ii) {
readFileP(files[ii]).then(function(data) {
text += data;
--counter;
if(counter===0) {
resolve(text);
}
});
}(i);
}
});
}).then(function(result) {
resolve(result);
});
});
} foo('./').then(function(data) {
console.log(data);
});

我了个去,怎么看起来比回调风格的还复杂?没错的确是这样,因为你还是在用回调思维写promise风格的代码,是个四不像。正宗的写法应该是这样的:

function foo(dir) {
var promise = readdirP(dir) .then(function(files) {
var arr=[];
for(var i=0, j=files.length; i<j; ++i) {
arr.push(readFileP(files[i]));
}
return Promise.all(arr);
}) .then(function(datas) {
return datas.join('');
}); return promise;
} foo('./').then(function(data) {
console.log(data);
});

这里关键就在于Promise.all()的使用。Promise.all(arr)接受一组promise为参数,即promise数组。当所有promise都变成resolved的时候就完成了,输出也是一个数组,即每个promise所resolve的值。如果任何一个promise变成rejected,则整个失败,可以在后面用catch捕获。标准写法:

//伪代码
var arr = [promise1, promise2, promise3];
Promise.all(arr)
.then(function(resultArr) {
使用resultArr;
})
.catch(function(error) {
console.log(error);
});

Promise.race()方法:

稍提一下Promise.race(arr)方法,用法跟Promise.all(arr)类似,只不过arr中任何一个promise变resolved/rejected的时候就结束,输出这个resolve/reject的值。这个方法的功能从它的名字就可以看出来。

最佳实践:

Promise流程最后一定要加个catch()捕获可能发生的错误。

then(fn)方法只接受函数作为的参数,fn如果是异步的,则必须要return一个promise对象;如果是同步的,则可以直接return一个value

function foo(arg) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve(arg + 1);
}, 1000);
});
} foo(0)
.then(foo)
.then(foo)
.then(function(arg) {
return arg +1;
})
.then(foo)
.then(function(arg) {
console.log(arg);
});

猜猜上述代码最后输出多少?foo被调用了4次,并且中间有一次同步arg+1的代码,所以最后输出5。这里的同步代码arg+1太简单只是为了演示,如果你的同步代码比较复杂而且中间可能抛出exception,那最好让同步代码也返回一个promise,这样就可以在最后catch里面捕获到,真是太爽了:

foo(0)
.then(foo)
.then(foo)
.then(function(arg) {
return Promise.resolve().then(function() {
return arg +1;
});
})
.then(foo)
.catch(function(err) {
console.log(err);
});

即把同步代码用Promise.resolve().then(function() { … } 进行包裹。Promise.resolve()是生成promise对象的快捷方法,不过它生成的promise对象初始状态就是resolved的。Promise.resolve()方法还可以带参数,这里不进行详述,大家可以自行去了解一下。

用上述方法写出来的流程,出错几率会大大减少。

说了这么久,该说重点了:)

堆积木:

返本溯源,promise是为了解决什么问题来着?对了,解决回调地狱,本质上是为了更加清晰的组织异步代码。Promise的精髓用法就是把一个个异步函数像积木一样按照它们的顺序堆积自来,可以串行可以并行,这种堆积木方式的组织流程相当灵活,可以组织出任意你的业务中需要的流程。这样说比较抽象,还是用例子吧:

(这是我实际项目中的一个真实例子)我有5个promise风格的异步函数fn1, fn2, fn3, fn4 和 fn5。fn3需要用到fn2的结果,fn4需要用到fn3的结果, fn5需要用到fn1, fn2, fn3和fn4的结果。是不是挺绕,应该怎么写?时间关系就不卖关子了。

var p1 = fn1(),
p2 = fn2();
p3 = p2.then(fn3);
p4 = p3.then(fn4); var arr = [p1, p2, p3, p4]; Promise.all(arr).then(fn5);

怎么样,是不是很神奇?发挥你的想象力,这些异步函数你可以随意组合,串行并行。

切记:组合的过程中每个异步函数通常只出现一次,除非你业务需要它使用不同的数据运行多次,否则如果出现多次,极有可能你已经掉坑里了:

//错误代码
var p1 = fn1(),
p2 = fn2();
p3 = fn2().then(fn3);
p4 = fn2().then(fn3).then(fn4); var arr = [p1, p2, p3, p4]; Promise.all(arr).then(fn5);

看起来两组代码似乎等价哦,呵呵,只不过错误代码中fn2会跑3次,fn3会跑2次。好好对比清楚:)

我在还没有领悟这种用法的时候是用这样直肠子的做法:

fn1()
.then(fn2)
.then(fn3)
.then(fn4)
.then(fn5);

哟?这不是更简单吗?错!因为fn1的输出在fn2, fn3和fn4中根本没用,但是还是必须捎带在他们每一个的输出结果里面; fn4根本不需要fn2的输出,但又要捎带在fn3里面以传给fn4最后给fn5。这样就造成这些函数深度耦合在一起,功能混乱。 所以记得promise不只能串行,也可以并行,就像堆积木一样非常灵活的进行组合。不知谁这么聪明发明了这种方法:)

转载请注明出处: http://www.cnblogs.com/chrischjh/p/4692743.html

『本集完』

Javascript异步编程之三Promise: 像堆积木一样组织你的异步流程的更多相关文章

  1. JS异步编程 (2) - Promise、Generator、async/await

    JS异步编程 (2) - Promise.Generator.async/await 上篇文章我们讲了下JS异步编程的相关知识,比如什么是异步,为什么要使用异步编程以及在浏览器中JS如何实现异步的.最 ...

  2. 转:[你必须知道的异步编程]C# 5.0 新特性——Async和Await使异步编程更简单

    本专题概要: 引言 同步代码存在的问题 传统的异步编程改善程序的响应 C# 5.0 提供的async和await使异步编程更简单  async和await关键字剖析 小结 一.引言 在之前的C#基础知 ...

  3. [你必须知道的异步编程]C# 5.0 新特性——Async和Await使异步编程更简单

    本专题概要: 引言 同步代码存在的问题 传统的异步编程改善程序的响应 C# 5.0 提供的async和await使异步编程更简单  async和await关键字剖析 小结 一.引言 在之前的C#基础知 ...

  4. JavaScript异步编程的Promise模式(转)

    异步模式在web编程中变得越来越重要,对于web主流语言Javascript来说,这种模式实现起来不是很利索,为此,许多Javascript库(比如 jQuery和Dojo)添加了一种称为promis ...

  5. javascript异步编程,promise概念

    javascript 异步编程 概述 采用单线程模式工作的原因: 避免多线dom操作同步问题,javascript的执行环境中负责执行代码的线程只有一个 内容概要 同步模式和异步模式 事件循环和消息队 ...

  6. JavaScript异步编程的Promise模式

    参考: http://www.infoq.com/cn/news/2011/09/js-promise http://www.cnblogs.com/rubylouvre/p/3495286.html ...

  7. 【.NET异步编程系列1】:await&async语法糖让异步编程如鱼得水

    前导 Asynchronous programming Model(APM)异步编程模型以BeginMethod(...) 和 EndMethod(...)结对出现. IAsyncResult Beg ...

  8. 深入理解JS异步编程三(promise)

    jQuery 原本写一个小动画我们可能是这样的 $('.animateEle').animate({ opacity:'.5' }, 4000,function(){ $('.animateEle2' ...

  9. ES6 Generator 异步编程解决方案&&&promise

    Generator: 是比promise更高级的解决方案 next   yield function 后加* 状态机 generator语法糖 长轮询  接口常查询 ================= ...

随机推荐

  1. 在notepad++中使用正则匹配功能(一-龥!-~) 中文[利刃篇]

    用正则时间越久,人就越懒,就越知道正则的强大.正则,不只是在代码里用到,在字符查找是也会用到,学会适当使用正则,将会使你的工作事办功倍!但是,中文却是一个砍,不容易过. 于是在用notepad++,也 ...

  2. Shell-9--条件测试

  3. Windows10开发手记-RelativePanel使用详解

    Windows 10已于7月29号面向全球发布,同时Universal Windows Platform(UWP) SDK也已正式放出,配合VS 2015我们可以开发出通用的Windows App. ...

  4. 【Redis面试题】Redis的字符串是怎么实现的?

    年前本人在找工作面试时在Redis相关问题上可栽了跟头.在面试前按常规套路准备了一下,比如 Redis 的常用5种数据结构,Redis持久化策略,Redis实现分布式锁,简单发布订阅等等都准备了,当时 ...

  5. 使用Masonry对UIScrollView自动布局

    之前使用Masonry对UIScrollView进行过约束,当时是遇到了问题的,怎么约束都不对,因为赶进度直接改用frame了也没有对问题深究.就这样过了很久.........,直到前一段换工作的时候 ...

  6. 《java并发编程实战》

    目录 对本书的赞誉 译者序 前 言 第1章 简介 1.1 并发简史 1.2 线程的优势 1.2.1 发挥多处理器的强大能力 1.2.2 建模的简单性 1.2.3 异步事件的简化处理 1.2.4 响应更 ...

  7. 插入排序——Insertion Sort

    基本思想: 在要排序的一组数中,假定前n-1个数已经排好序,现在将第n个数插到前面的有序数列中,使得这n个数也是排好顺序的.如此反复循环,直到全部排好顺序. 过程: 平均时间复杂度:O(n2) jav ...

  8. android_安装包_NoClassDefFoundError

    说说这个问题出现的地方吧: 能够成功的打包安装包,但是在安装包安装后,准备运行时出现了这个问题. 查看了这篇文章,讲得有理有据,并没有解决我的问题. 通过谷歌查找到这个stackoverflow,解决 ...

  9. PostgreSQL Json字段作为查询条件案例

    业务扩展字段在数据库中经常会使用json格式的数据来存储,这就涉及到一个头疼的问题,假设要使用扩展字段里的某个值作为查询条件怎么办,原来PostgreSQL本身就支持这种查询方式. 例子:假设业务扩展 ...

  10. linux装tomcat遇到的坑

    最开始通过apt-get安装,各种毛病 然后下载tar.gz压缩包解压使用,运行startup.sh可以启动,但是看日志发现 Tomcat启动时卡在 INFO HostConfig.deployDir ...