第三章: Promise - Promise 模式

优质
小牛编辑
152浏览
2023-12-01

Promise模式

我们已经隐含地看到了使用Promise链的顺序模式(这个-然后-这个-然后-那个的流程控制),但是我们还可以在Promise的基础上抽象出许多其他种类的异步模式。这些模式用于简化异步流程控制的的表达——它可以使我们的代码更易于推理并且更易于维护——即便是我们程序中最复杂的部分。

有两个这样的模式被直接编码在ES6原生的Promise实现中,所以我们免费的得到了它们,来作为我们其他模式的构建块儿。

Promise.all([ .. ])

在一个异步序列(Promise链)中,在任何给定的时刻都只有一个异步任务在被协调——第2步严格地接着第1步,而第3步严格地接着第2步。但要是并发(也叫“并行地”)地去做两个或以上的步骤呢?

用经典的编程术语,一个“门(gate)”是一种等待两个或更多并行/并发任务都执行完再继续的机制。它们完成的顺序无关紧要,只是它们不得不都完成才能让门打开,继而让流程控制通过。

在Promise API中,我们称这种模式为all([ .. ])

比方说你想同时发起两个Ajax请求,在发起第三个Ajax请求发起之前,等待它们都完成,而不管它们的顺序。考虑这段代码:

  1. // `request(..)`是一个兼容Promise的Ajax工具
  2. // 就像我们在本章早前定义的
  3. var p1 = request( "http://some.url.1/" );
  4. var p2 = request( "http://some.url.2/" );
  5. Promise.all( [p1,p2] )
  6. .then( function(msgs){
  7. // `p1`和`p2`都已完成,这里将它们的消息传入
  8. return request(
  9. "http://some.url.3/?v=" + msgs.join(",")
  10. );
  11. } )
  12. .then( function(msg){
  13. console.log( msg );
  14. } );

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)”需要至少一个“选手”,所以如果你传入一个空的arrayrace([..])的主Promise将不会立即解析,反而是永远不会被解析。这是砸自己的脚!ES6应当将它规范为要么完成,要么拒绝,或者要么抛出某种同步错误。不幸的是,因为在ES6的Promise之前的Promise库的优先权高,他们不得不把这个坑留在这儿,所以要小心绝不要传入一个空array

让我们重温刚才的并发Ajax的例子,但是在p1p2竞合的环境下:

  1. // `request(..)`是一个兼容Promise的Ajax工具
  2. // 就像我们在本章早前定义的
  3. var p1 = request( "http://some.url.1/" );
  4. var p2 = request( "http://some.url.2/" );
  5. Promise.race( [p1,p2] )
  6. .then( function(msg){
  7. // `p1`或`p2`会赢得竞合
  8. return request(
  9. "http://some.url.3/?v=" + msg
  10. );
  11. } )
  12. .then( function(msg){
  13. console.log( msg );
  14. } );

因为只有一个Promise会胜出,所以完成的值是一个单独的消息,而不是一个像Promise.all([ .. ])中那样的array

超时竞合

我们早先看过这个例子,描述Promise.race([ .. ])如何能够用于表达“promise超时”模式:

  1. // `foo()`是一个兼容Promise
  2. // `timeoutPromise(..)`在早前定义过,
  3. // 返回一个在指定延迟之后会被拒绝的Promise
  4. // 为`foo()`设置一个超时
  5. Promise.race( [
  6. foo(), // 尝试`foo()`
  7. timeoutPromise( 3000 ) // 给它3秒钟
  8. ] )
  9. .then(
  10. function(){
  11. // `foo(..)`及时地完成了!
  12. },
  13. function(err){
  14. // `foo()`要么是被拒绝了,要么就是没有及时完成
  15. // 可以考察`err`来知道是哪一个原因
  16. }
  17. );

这种超时模式在绝大多数情况下工作的很好。但这里有一些微妙的细节要考虑,而且坦率的说它们对于Promise.race([ .. ])Promise.all([ .. ])都同样需要考虑。

“Finally”

要问的关键问题是,“那些被丢弃/忽略的promise发生了什么?”我们不是从性能的角度在问这个问题——它们通常最终会变成垃圾回收的合法对象——而是从行为的角度(副作用等等)。Promise不能被取消——而且不应当被取消,因为那会摧毁本章稍后的“Promise不可取消”一节中要讨论的外部不可变性——所以它们只能被无声地忽略。

但如果前面例子中的foo()占用了某些资源,但超时首先触发而且导致这个promise被忽略了呢?这种模式中存在某种东西可以在超时后主动释放被占用的资源,或者取消任何它可能带来的副作用吗?要是你想做的全部只是记录下foo()超时的事实呢?

一些开发者提议,Promise需要一个finally(..)回调注册机制,它总是在Promise解析时被调用,而且允许你制定任何可能的清理操作。在当前的语言规范中它还不存在,但它可能会在ES7+中加入。我们不得不边走边看了。

它看起来可能是这样:

  1. var p = Promise.resolve( 42 );
  2. p.then( something )
  3. .finally( cleanup )
  4. .then( another )
  5. .finally( cleanup );

注意: 在各种Promise库中,finally(..)依然会创建并返回一个新的Promise(为了使链条延续下去)。如果cleanup(..)函数返回一个Promise,它将会链入链条,这意味着你可能还有我们刚才讨论的未处理拒绝的问题。

同时,我们可以制造一个静态的帮助工具来让我们观察(但不干涉)Promise的解析:

  1. // 填补的安全检查
  2. if (!Promise.observe) {
  3. Promise.observe = function(pr,cb) {
  4. // 从侧面观察`pr`的解析
  5. pr.then(
  6. function fulfilled(msg){
  7. // 异步安排回调(作为Job)
  8. Promise.resolve( msg ).then( cb );
  9. },
  10. function rejected(err){
  11. // 异步安排回调(作为Job)
  12. Promise.resolve( err ).then( cb );
  13. }
  14. );
  15. // 返回原本的promise
  16. return pr;
  17. };
  18. }

这是我们在前面的超时例子中如何使用它:

  1. Promise.race( [
  2. Promise.observe(
  3. foo(), // 尝试`foo()`
  4. function cleanup(msg){
  5. // 在`foo()`之后进行清理,即便它没有及时完成
  6. }
  7. ),
  8. timeoutPromise( 3000 ) // 给它3秒钟
  9. ] )

这个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([..]):

  1. // 填补的安全检查
  2. if (!Promise.first) {
  3. Promise.first = function(prs) {
  4. return new Promise( function(resolve,reject){
  5. // 迭代所有的promise
  6. prs.forEach( function(pr){
  7. // 泛化它的值
  8. Promise.resolve( pr )
  9. // 无论哪一个首先成功完成,都由它来解析主promise
  10. .then( resolve );
  11. } );
  12. } );
  13. };
  14. }

注意: 这个first(..)的实现不会在它所有的promise都被拒绝时拒绝;它会简单地挂起,很像Promise.race([])。如果需要,你可以添加一些附加逻辑来追踪每个promise的拒绝,而且如果所有的都被拒绝,就在主promise上调用reject()。我们将此作为练习留给读者。

并发迭代

有时候你想迭代一个Promise的列表,并对它们所有都实施一些任务,就像你可以对同步的array做的那样(比如,forEach(..)map(..)some(..),和every(..))。如果对每个Promise实施的操作根本上是同步的,它们工作的很好,正如我们在前面的代码段中用过的forEach(..)

但如果任务在根本上是异步的,或者可以/应当并发地实施,你可以使用许多库提供的异步版本的这些工具方法。

比如,让我们考虑一个异步的map(..)工具,它接收一个array值(可以是Promise或任何东西),外加一个对数组中每一个值实施的函数(任务)。map(..)本身返回一个promise,它的完成值是一个持有每个任务的异步完成值的array(以与映射(mapping)相同的顺序):

  1. if (!Promise.map) {
  2. Promise.map = function(vals,cb) {
  3. // 一个等待所有被映射的promise的新promise
  4. return Promise.all(
  5. // 注意:普通的数组`map(..)`,
  6. // 将值的数组变为promise的数组
  7. vals.map( function(val){
  8. // 将`val`替换为一个在`val`
  9. // 异步映射完成后才解析的新promise
  10. return new Promise( function(resolve){
  11. cb( val, resolve );
  12. } );
  13. } )
  14. );
  15. };
  16. }

注意: 在这种map(..)的实现中,你无法表示异步拒绝,但如果一个在映射的回调内部发生一个同步的异常/错误,那么Promise.map(..)返回的主Promise就会拒绝。

让我们描绘一下对一组Promise(不是简单的值)使用map(..)

  1. var p1 = Promise.resolve( 21 );
  2. var p2 = Promise.resolve( 42 );
  3. var p3 = Promise.reject( "Oops" );
  4. // 将列表中的值翻倍,即便它们在Promise中
  5. Promise.map( [p1,p2,p3], function(pr,done){
  6. // 确保列表中每一个值都是Promise
  7. Promise.resolve( pr )
  8. .then(
  9. // 将值作为`v`抽取出来
  10. function(v){
  11. // 将完成的`v`映射到新的值
  12. done( v * 2 );
  13. },
  14. // 或者,映射到promise的拒绝消息上
  15. done
  16. );
  17. } )
  18. .then( function(vals){
  19. console.log( vals ); // [42,84,"Oops"]
  20. } );