3.6 Promise 模式
前文我们无疑已经看到了使用 Promise 链的顺序模式(this-then-this-then-that 流程控制),但是可以基于 Promise 构建的异步模式抽象还有很多变体。这些模式是为了简化异步流程控制,这使得我们的代码更容易追踪和维护,即使在程序中最复杂的部分也是如此。
原生 ES6 Promise 实现中直接支持了两个这样的模式,所以我们可以免费得到它们,用作构建其他模式的基本块。
3.6.1 Promise.all([ .. ])
在异步序列中(Promise 链),任意时刻都只能有一个异步任务正在执行——步骤 2 只能在步骤 1 之后,步骤 3 只能在步骤 2 之后。但是,如果想要同时执行两个或更多步骤(也就是“并行执行”),要怎么实现呢?
在经典的编程术语中,门(gate)是这样一种机制要等待两个或更多并行 / 并发的任务都完成才能继续。它们的完成顺序并不重要,但是必须都要完成,门才能打开并让流程控制继续。
在 Promise API 中,这种模式被称为 all([ .. ]) 。
假定你想要同时发送两个 Ajax 请求,等它们不管以什么顺序全部完成之后,再发送第三个 Ajax 请求。考虑:
// request(..)是一个Promise-aware 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([ .. ]) 需要一个参数,是一个数组,通常由 Promise 实例组成。从 Promise.all([ .. ]) 调用返回的 promise 会收到一个完成消息(代码片段中的 msg )。这是一个由所有传入 promise 的完成消息组成的数组,与指定的顺序一致(与完成顺序无关)。
严格说来,传给 Promise.all([ .. ]) 的数组中的值可以是 Promise、thenable,甚至是立即值。就本质而言,列表中的每个值都会通过 Promise. resolve(..) 过滤,以确保要等待的是一个真正的 Promise,所以立即值会被规范化为为这个值构建的 Promise。如果数组是空的,主 Promise 就会立即完成。
从 Promise.all([ .. ]) 返回的主 promise 在且仅在所有的成员 promise 都完成后才会完成。如果这些 promise 中有任何一个被拒绝的话,主 Promise.all([ .. ]) promise 就会立即被拒绝,并丢弃来自其他所有 promise 的全部结果。
永远要记住为每个 promise 关联一个拒绝 / 错误处理函数,特别是从 Promise.all([ .. ]) 返回的那一个。
3.6.2 Promise.race([ .. ])
尽管 Promise.all([ .. ]) 协调多个并发 Promise 的运行,并假定所有 Promise 都需要完成,但有时候你会想只响应“第一个跨过终点线的 Promise”,而抛弃其他 Promise。
这种模式传统上称为门闩,但在 Promise 中称为竞态。
虽然“只有第一个到达终点的才算胜利”这个比喻很好地描述了其行为特性,但遗憾的是,由于竞态条件通常被认为是程序中的 bug(参见第 1 章),所以从某种程度上说,“竞争”这个词已经是一个具有固定意义的术语了。不要混淆了 Promise.race([..]) 和竞态条件。
Promise.race([ .. ]) 也接受单个数组参数。这个数组由一个或多个 Promise、thenable 或立即值组成。立即值之间的竞争在实践中没有太大意义,因为显然列表中的第一个会获胜,就像赛跑中有一个选手是从终点开始比赛一样!
与 Promise.all([ .. ]) 类似,一旦有任何一个 Promise 决议为完成,Promise.race([ .. ]) 就会完成;一旦有任何一个 Promise 决议为拒绝,它就会拒绝。
一项竞赛需要至少一个“参赛者”。所以,如果你传入了一个空数组,主 race([..]) Promise 永远不会决议,而不是立即决议。这很容易搬起石头砸自己的脚! ES6 应该指定它完成或拒绝,抑或只是抛出某种同步错误。遗憾的是,因为 Promise 库在时间上早于 ES6 Promise ,它们不得已遗留了这个问题,所以,要注意,永远不要递送空数组。
再回顾一下前面的并发 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([ .. ]) 那样的是一个数组。
1. 超时竞赛
我们之前看到过这个例子,其展示了如何使用 Promise.race([ .. ]) 表达 Promise 超时模式:
// foo()是一个支持Promise的函数 // 前面定义的timeoutPromise(..)返回一个promise, // 这个promise会在指定延时之后拒绝 // 为foo()设定超时 Promise.race( [ foo(), // 启动foo() timeoutPromise( 3000 ) // 给它3秒钟 ] ) .then( function(){ // foo(..)按时完成! }, function(err){ // 要么foo()被拒绝,要么只是没能够按时完成, // 因此要查看err了解具体原因 } );
在多数情况下,这个超时模式能够很好地工作。但是,还有一些微妙的情况需要考虑,并且坦白地说,对于 Promise.race([ .. ]) 和 Promise.all([ .. ]) 也都是如此。
2. finally
一个关键问题是:“那些被丢弃或忽略的 promise 会发生什么呢?”我们并不是从性能的角度提出这个问题的——通常最终它们会被垃圾回收——而是从行为的角度(副作用等)。Promise 不能被取消,也不应该被取消,因为那会摧毁 3.8.5 节讨论的外部不变性原则,所以它们只能被默默忽略。
那么如果前面例子中的 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 就会被连接到链中,这意味着这里还是会有前面讨论过的未处理拒绝问题。
同时,我们可以构建一个静态辅助工具来支持查看(而不影响)Promise 的决议:
// polyfill安全的guard检查 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 不会被意外默默忽略的情况。
3.6.3 all([ .. ]) 和 race([ .. ]) 的变体
虽然原生 ES6 Promise 中提供了内建的 Promise.all([ .. ]) 和 Promise.race([ .. ]) ,但这些语义还有其他几个常用的变体模式。
· none([ .. ])
这个模式类似于 all([ .. ]) ,不过完成和拒绝的情况互换了。所有的 Promise 都要被拒绝,即拒绝转化为完成值,反之亦然。
· any([ .. ])
这个模式与 all([ .. ]) 类似,但是会忽略拒绝,所以只需要完成一个而不是全部。
· first([ .. ])
这个模式类似于与 any([ .. ]) 的竞争,即只要第一个 Promise 完成,它就会忽略后续的任何拒绝和完成。
· last([ .. ])
这个模式类似于 first([ .. ]) ,但却是只有最后一个完成胜出。
有些 Promise 抽象库提供了这些支持,但也可以使用 Promise、race([ .. ]) 和 all([ .. ]) 这些机制,你自己来实现它们。
比如,可以像这样定义 first([ .. ]) :
// polyfill安全的guard检查 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 都被拒绝,就在主 promise 上调用 reject() 。这个实现留给你当练习。
3.6.4 并发迭代
有些时候会需要在一列 Promise 中迭代,并对所有 Promise 都执行某个任务,非常类似于对同步数组可以做的那 样(比 如 forEach(..) 、map(..) 、some(..) 和 every(..) )。如果要对每个 Promise 执行的任务本身是同步的,那这些工具就可以工作,就像前面代码中的 forEach(..) 。
但如果这些任务从根本上是异步的,或者可以 / 应该并发执行,那你可以使用这些工具的异步版本,许多库中提供了这样的工具。
举例来说,让我们考虑一下一个异步的 map(..) 工具。它接收一个数组的值(可以是 Promise 或其他任何值),外加要在每个值上运行一个函数(任务)作为参数。map(..) 本身返回一个 promise,其完成值是一个数组,该数组(保持映射顺序)保存任务执行之后的异步完成值:
if (!Promise.map) { Promise.map = function(vals,cb) { // 一个等待所有map的promise的新promise return Promise.all( // 注:一般数组map(..)把值数组转换为 promise数组 vals.map( function(val){ // 用val异步map之后决议的新promise替换val return new Promise( function(resolve){ cb( val, resolve ); } ); } ) ); }; }
在这个 map(..) 实现中,不能发送异步拒绝信号,但如果在映射的回调(cb(..) )内出现同步的异常或错误,主 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){ // map完成的v到新值 done( v * 2 ); }, // 或者map到promise拒绝消息 done ); } ) .then( function(vals){ console.log( vals ); // [42,84,"Oops"] } );
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论