背景
相信之前用过JavaScript的朋友都碰到过异步回调地狱(callback hell),N多个回调的嵌套不仅让代码读起来十分困难,维护起来也很不方便。 其实C#在Task 出现之前也是有类似场景的,Async Programming Mode时代,用Action 和Func 做回调也很流行,不过也是意识到太多的回调嵌套代码可读性差且维护不易,微软引入了Task 和Task-based Async Pattern。 虽然不知道是哪个语言最早有这个概念,但相信是C#把async await 带到流行语言的舞台,接着其他语言也以不同的形式支持async await ,如Python, Dart, Swift等。 JavaScript同样在ES6开始支持Promise 和Generator ,并在ES7中提出支持async await 的议案。
这篇先来看看Promise:
Promise的特点
Promise 之于TypeScript,相当于Task 之于C#,只有返回Promise 的函数才能使用async await 。
Promise 其实就是一个可以获取异步结果,并封装了一些异步操作的对象。 有三个状态:
pending : 进行中
resolved : 成功
rejected : 失败 并且这三个状态只有两种转换:pending ->resolved 、pending ->rejected ,不是成功就是失败,并没有多余的状态转换。 这两种转换都是由异步返回的结果给定的,成功取回数据就是resolved ,取数据出异常就是rejected 。 也因此,这转换过后的结果就是固定的了,不可能在转换过后还会变回pending 或其他状态。
Promise 不能在任务进行中取消,只能等结果返回,这点上不如C#的Task ,Task 可以通过CancelTaskToken 来取消任务。
Promise的使用
可以直接new一个Promise 对象,构造函数的参数是一个有两个参数的函数。 这两个参数一个是resove ,用来在异步操作成功后调用,并把异步结果传出去,调用resove 后状态就由pending ->resolved 。 另一个是reject ,用来在失败或异常时调用,并把错误消息传出去,调用reject 后状态由pending ->rejected 。
var promise = new Promise(function(resolve, reject) {
});
通常需要在成功或失败后做一些操作,这时需要then 来做这个事,then 可以有两个函数参数,第一个是成功后调用的,第二个是失败调用的,第二个是可选的。 另外,then 返回的也是一个Promise,不过不是原来的那个,而是新new出来的,这样可以链式调用,then 后面再接then 。
嵌套的Promise
在实际场景中,我们可能需要在一个异步操作后再接个异步操作,这样就会有Promise 的嵌套操作。 下面的代码显示的是Promise 的嵌套操作:
p1 先打印"start",延时两秒打印"p1"。
p2 在p1 完成后延时两秒打印"p2"。
function delay(): Promise<void>{
return new Promise<void>((resolve, reject)=>{setTimeout(()=>resolve(), 2000)});
}
let p1 = new Promise((resolve, reject) => {
console.info('start');
delay().then(()=>{
console.info('p1');
resolve()
});
});
let p2 = new Promise((resolve, reject) => {
p1.then(()=>delay().then(()=>resolve()));
});
p2.then(()=>console.info('p2'));
异常处理
上面提到Promise 出错时把状态变为rejected 并把错误消息传给reject 函数,在then 里面调用reject 函数就可以显示异常。 不过这样写显得不是很友好,Promise 还有个catch 函数专门用来处理错误异常。 而且Promise 的异常是冒泡传递的,最后面写一个catch 就可以捕获到前面所有promise可能发生的异常,如果用reject 就需要每个都写。 所以reject 函数一般就不需要在then 里面写,在后面跟个catch 就可以了。
new Promise(function(resolve, reject) {
throw new Error('error');
}).catch(function(error) {
console.info(error);
也如上面所说状态只有两种变化且一旦变化就固定下来,所以如果已经在Promise 里执行了resolve ,再throw异常是没用的,catch不到,因为状态已经变成resolved 。
new Promise(function(resolve, reject) {
resolve('success');
throw new Error('error');
}).catch(function(error) {
console.info(error);
另外,catch 里的代码也可能出异常,所以catch 后面也还可以跟catch 的议案。
new Promise(function(resolve, reject) {
throw new Error('error');
}).catch(function(error) {
console.info(error);
BlueBird的 finally 和 done
异常的try...catch 后面可以跟finally 来执行必须要执行的代码,Promise 原生并不支持,可以引入BlueBird的扩展库来支持。 另外还有done 在最后面来表示执行结束并抛出可能出现的异常,比如最后一个catch 代码块里的异常。
let p = new Promise(function(resolve, reject) {
x = 2;
并行执行Promise
虽然JavaScript是单线程语言,但并不妨碍它执行一些IO并行操作,如不阻塞发出http request,然后异步等待。
Promise 除了用then 来顺序执行外,也同样可以不阻塞同时执行多个Promise 然后等所有结果返回再进行后续操作。 C#的Task 有个WhenAll 的静态方法来做这个事,Promise 则是用all 方法达到同样目的。
all 方法接受实现Iterator接口的对象,比如数组。
let p = Promise.all([p1, p2, p3]);
all 返回的是一个新的Promise - p,p的状态是由p1, p2, p3同时决定的:
p.resolved = p1.resolve && p2.resolve && p3.resolve
p.rejected = p1.rejected || p2.rejected || p3.rejected
也就是说p的成功需要p1,p2,p3都成功,而只要p1, p2, p3里有任何一个失败则p失败并退出。
Promise 还有一个方法race 同样是并行执行多个Promise ,不同于all 的是它的成功状态和错误状态一样,只要有一个成功就成功,如同C# Task的Any 方法。
|
请发表评论