返回介绍

第一部分 类型和语法

第二部分 异步和性能

3.6 Promise 模式

发布于 2023-05-24 16:38:21 字数 8129 浏览 0 评论 0 收藏 0

前文我们无疑已经看到了使用 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文