第三章: Promise - Promise 模式
Promise模式
我们已经隐含地看到了使用Promise链的顺序模式(这个-然后-这个-然后-那个的流程控制),但是我们还可以在Promise的基础上抽象出许多其他种类的异步模式。这些模式用于简化异步流程控制的的表达——它可以使我们的代码更易于推理并且更易于维护——即便是我们程序中最复杂的部分。
有两个这样的模式被直接编码在ES6原生的Promise
实现中,所以我们免费的得到了它们,来作为我们其他模式的构建块儿。
Promise.all([ .. ])
在一个异步序列(Promise链)中,在任何给定的时刻都只有一个异步任务在被协调——第2步严格地接着第1步,而第3步严格地接着第2步。但要是并发(也叫“并行地”)地去做两个或以上的步骤呢?
用经典的编程术语,一个“门(gate)”是一种等待两个或更多并行/并发任务都执行完再继续的机制。它们完成的顺序无关紧要,只是它们不得不都完成才能让门打开,继而让流程控制通过。
在Promise API中,我们称这种模式为all([ .. ])
。
比方说你想同时发起两个Ajax请求,在发起第三个Ajax请求发起之前,等待它们都完成,而不管它们的顺序。考虑这段代码:
// `request(..)`是一个兼容Promise的Ajax工具
// 就像我们在本章早前定义的
var p1 = request( "http://some.url.1/" );
var p2 = request( "http://some.url.2/" );
Promise.all( [p1,p2] )
.then( function(msgs){
// `p1`和`p2`都已完成,这里将它们的消息传入
return request(
"http://some.url.3/?v=" + msgs.join(",")
);
} )
.then( function(msg){
console.log( msg );
} );
Promise.all([ .. ])
期待一个单独的参数,一个array
,一般由Promise的实例组成。从Promise.all([ .. ])
返回的promise将会收到完成的消息(在这段代码中是msgs
),它是一个由所有被传入的promise的完成消息按照被传入的顺序构成的array
(与完成的顺序无关)。
注意: 技术上讲,被传入Promise.all([ .. ])
的array
的值可以包括Promise,thenable,甚至是立即值。这个列表中的每一个值都实质上通过Promise.resolve(..)
来确保它是一个可以被等待的纯粹的Promise,所以一个立即值将被范化为这个值的一个Promise。如果这个array
是空的,主Promise将会立即完成。
从Promise.resolve(..)
返回的主Promise将会在所有组成它的promise完成之后才会被完成。如果其中任意一个promise被拒绝,Promise.all([ .. ])
的主Promise将立即被拒绝,并放弃所有其他promise的结果。
要记得总是给每个promise添加拒绝/错误处理器,即使和特别是那个从Promise.all([ .. ])
返回的promise。
Promise.race([ .. ])
虽然Promise.all([ .. ])
并发地协调多个Promise并假定它们都需要被完成,但是有时候你只想应答“冲过终点的第一个Promise”,而让其他的Promise被丢弃。
这种模式经典地被称为“闩”,但在Promise中它被称为一个“竞合(race)”。
警告: 虽然“只有第一个冲过终点的算赢”是一个非常合适被比喻,但不幸的是“竞合(race)”是一个被占用的词,因为“竞合状态(race conditions)”通常被认为是程序中的Bug(见第一章)。不要把Promise.race([ .. ])
与“竞合状态(race conditions)”搞混了。
“竞合状态(race conditions)”也期待一个单独的array
参数,含有一个或多个Promise,thenable,或立即值。与立即值进行竞合并没有多大实际意义,因为很明显列表中的第一个会胜出——就像赛跑时有一个选手在终点线上起跑!
和Promise.all([ .. ])
相似,Promise.race([ .. ])
将会在任意一个Promise解析为完成时完成,而且它会在任意一个Promise解析为拒绝时拒绝。
注意: 一个“竞合(race)”需要至少一个“选手”,所以如果你传入一个空的array
,race([..])
的主Promise将不会立即解析,反而是永远不会被解析。这是砸自己的脚!ES6应当将它规范为要么完成,要么拒绝,或者要么抛出某种同步错误。不幸的是,因为在ES6的Promise
之前的Promise库的优先权高,他们不得不把这个坑留在这儿,所以要小心绝不要传入一个空array
。
让我们重温刚才的并发Ajax的例子,但是在p1
和p2
竞合的环境下:
// `request(..)`是一个兼容Promise的Ajax工具
// 就像我们在本章早前定义的
var p1 = request( "http://some.url.1/" );
var p2 = request( "http://some.url.2/" );
Promise.race( [p1,p2] )
.then( function(msg){
// `p1`或`p2`会赢得竞合
return request(
"http://some.url.3/?v=" + msg
);
} )
.then( function(msg){
console.log( msg );
} );
因为只有一个Promise会胜出,所以完成的值是一个单独的消息,而不是一个像Promise.all([ .. ])
中那样的array
。
超时竞合
我们早先看过这个例子,描述Promise.race([ .. ])
如何能够用于表达“promise超时”模式:
// `foo()`是一个兼容Promise
// `timeoutPromise(..)`在早前定义过,
// 返回一个在指定延迟之后会被拒绝的Promise
// 为`foo()`设置一个超时
Promise.race( [
foo(), // 尝试`foo()`
timeoutPromise( 3000 ) // 给它3秒钟
] )
.then(
function(){
// `foo(..)`及时地完成了!
},
function(err){
// `foo()`要么是被拒绝了,要么就是没有及时完成
// 可以考察`err`来知道是哪一个原因
}
);
这种超时模式在绝大多数情况下工作的很好。但这里有一些微妙的细节要考虑,而且坦率的说它们对于Promise.race([ .. ])
和Promise.all([ .. ])
都同样需要考虑。
“Finally”
要问的关键问题是,“那些被丢弃/忽略的promise发生了什么?”我们不是从性能的角度在问这个问题——它们通常最终会变成垃圾回收的合法对象——而是从行为的角度(副作用等等)。Promise不能被取消——而且不应当被取消,因为那会摧毁本章稍后的“Promise不可取消”一节中要讨论的外部不可变性——所以它们只能被无声地忽略。
但如果前面例子中的foo()
占用了某些资源,但超时首先触发而且导致这个promise被忽略了呢?这种模式中存在某种东西可以在超时后主动释放被占用的资源,或者取消任何它可能带来的副作用吗?要是你想做的全部只是记录下foo()
超时的事实呢?
一些开发者提议,Promise需要一个finally(..)
回调注册机制,它总是在Promise解析时被调用,而且允许你制定任何可能的清理操作。在当前的语言规范中它还不存在,但它可能会在ES7+中加入。我们不得不边走边看了。
它看起来可能是这样:
var p = Promise.resolve( 42 );
p.then( something )
.finally( cleanup )
.then( another )
.finally( cleanup );
注意: 在各种Promise库中,finally(..)
依然会创建并返回一个新的Promise(为了使链条延续下去)。如果cleanup(..)
函数返回一个Promise,它将会链入链条,这意味着你可能还有我们刚才讨论的未处理拒绝的问题。
同时,我们可以制造一个静态的帮助工具来让我们观察(但不干涉)Promise的解析:
// 填补的安全检查
if (!Promise.observe) {
Promise.observe = function(pr,cb) {
// 从侧面观察`pr`的解析
pr.then(
function fulfilled(msg){
// 异步安排回调(作为Job)
Promise.resolve( msg ).then( cb );
},
function rejected(err){
// 异步安排回调(作为Job)
Promise.resolve( err ).then( cb );
}
);
// 返回原本的promise
return pr;
};
}
这是我们在前面的超时例子中如何使用它:
Promise.race( [
Promise.observe(
foo(), // 尝试`foo()`
function cleanup(msg){
// 在`foo()`之后进行清理,即便它没有及时完成
}
),
timeoutPromise( 3000 ) // 给它3秒钟
] )
这个Promise.observe(..)
帮助工具只是描述你如何在不干扰Promise的情况下观测它的完成。其他的Promise库有他们自己的解决方案。不论你怎么做,你都将很可能有个地方想用来确认你的Promise没有意外地被无声地忽略掉。
all([ .. ]) 与 race([ .. ]) 的变种
原生的ES6Promise带有内建的Promise.all([ .. ])
和Promise.race([ .. ])
,这里还有几个关于这些语义的其他常用的变种模式:
none([ .. ])
很像all([ .. ])
,但是完成和拒绝被转置了。所有的Promise都需要被拒绝——拒绝变成了完成值,反之亦然。any([ .. ])
很像all([ .. ])
,但它忽略任何拒绝,所以只有一个需要完成即可,而不是它们所有的。first([ .. ])
像是一个带有any([ .. ])
的竞合,它忽略任何拒绝,而且一旦有一个Promise完成时,它就立即完成。last([ .. ])
很像first([ .. ])
,但是只有最后一个完成胜出。
某些Promise抽象工具库提供这些方法,但你也可以用Promise机制的race([ .. ])
和all([ .. ])
,自己定义他们。
比如,这是我们如何定义first([..])
:
// 填补的安全检查
if (!Promise.first) {
Promise.first = function(prs) {
return new Promise( function(resolve,reject){
// 迭代所有的promise
prs.forEach( function(pr){
// 泛化它的值
Promise.resolve( pr )
// 无论哪一个首先成功完成,都由它来解析主promise
.then( resolve );
} );
} );
};
}
注意: 这个first(..)
的实现不会在它所有的promise都被拒绝时拒绝;它会简单地挂起,很像Promise.race([])
。如果需要,你可以添加一些附加逻辑来追踪每个promise的拒绝,而且如果所有的都被拒绝,就在主promise上调用reject()
。我们将此作为练习留给读者。
并发迭代
有时候你想迭代一个Promise的列表,并对它们所有都实施一些任务,就像你可以对同步的array
做的那样(比如,forEach(..)
,map(..)
,some(..)
,和every(..)
)。如果对每个Promise实施的操作根本上是同步的,它们工作的很好,正如我们在前面的代码段中用过的forEach(..)
。
但如果任务在根本上是异步的,或者可以/应当并发地实施,你可以使用许多库提供的异步版本的这些工具方法。
比如,让我们考虑一个异步的map(..)
工具,它接收一个array
值(可以是Promise或任何东西),外加一个对数组中每一个值实施的函数(任务)。map(..)
本身返回一个promise,它的完成值是一个持有每个任务的异步完成值的array
(以与映射(mapping)相同的顺序):
if (!Promise.map) {
Promise.map = function(vals,cb) {
// 一个等待所有被映射的promise的新promise
return Promise.all(
// 注意:普通的数组`map(..)`,
// 将值的数组变为promise的数组
vals.map( function(val){
// 将`val`替换为一个在`val`
// 异步映射完成后才解析的新promise
return new Promise( function(resolve){
cb( val, resolve );
} );
} )
);
};
}
注意: 在这种map(..)
的实现中,你无法表示异步拒绝,但如果一个在映射的回调内部发生一个同步的异常/错误,那么Promise.map(..)
返回的主Promise就会拒绝。
让我们描绘一下对一组Promise(不是简单的值)使用map(..)
:
var p1 = Promise.resolve( 21 );
var p2 = Promise.resolve( 42 );
var p3 = Promise.reject( "Oops" );
// 将列表中的值翻倍,即便它们在Promise中
Promise.map( [p1,p2,p3], function(pr,done){
// 确保列表中每一个值都是Promise
Promise.resolve( pr )
.then(
// 将值作为`v`抽取出来
function(v){
// 将完成的`v`映射到新的值
done( v * 2 );
},
// 或者,映射到promise的拒绝消息上
done
);
} )
.then( function(vals){
console.log( vals ); // [42,84,"Oops"]
} );