资讯专栏INFORMATION COLUMN

Promise 中的三兄弟 .all(), .race(), .allSettled()

vspiders / 1113人阅读

摘要:对于的来说基元函数包括组合函数的类型签名返回情况完成如果传入的可迭代对象为空,会同步地返回一个已完成状态的。相反,如果是在指定的时间之后完成,刚返回结果就是一个拒绝状态的从而触发方法指定的回调函数。在行中,对每个小任务得到的结果进行汇总。

为了保证的可读性,本文采用意译而非直译。

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

从ES6 开始,我们大都使用的是 Promise.all()Promise.race()Promise.allSettled() 提案已经到第4阶段,因此将会成为ECMAScript 2020的一部分。

1.概述

Promise.all(promises: Iterable>): Promise>

Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果

Promise.race(promises: Iterable>): Promise

Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。

Promise.allSettled(promises: Iterable>): Promise>>

Promise.allSettled()方法返回一个promise,该promise在所有给定的promise已被解析或被拒绝后解析,并且每个对象都描述每个promise的结果。

回顾: Promise 状态

给定一个返回Promise的异步操作,以下这些是Promise的可能状态:

pending: 初始状态,既不是成功,也不是失败状态。

fulfilled: 意味着操作成功完成。

rejected: 意味着操作失败。

Settled: Promise要么被完成,要么被拒绝。Promise一旦达成,它的状态就不再改变。

3.什么是组合

又称部分-整体模式,将对象整合成树形结构以表示“部分整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性,它基于两种函数:

基元函数(简短:基元)创建原子块。

组合函数(简称:组合)将原子和/或复合件组合在一起以形成复合件。

对于 JS 的 Promises 来说

基元函数包括:Promise.resolve()Promise.reject()

组合函数:Promise.all(), Promise.race(), Promise.allSettled()

4. Promise.all()

Promise.all()的类型签名:

Promise.all(promises: Iterable>): Promise>

返回情况:

完成(Fulfillment):
如果传入的可迭代对象为空,Promise.all 会同步地返回一个已完成(resolved)状态的promise
如果所有传入的 promise 都变为完成状态,或者传入的可迭代对象内没有 promisePromise.all 返回的 promise 异步地变为完成。
在任何情况下,Promise.all 返回的 promise 的完成状态的结果都是一个数组,它包含所有的传入迭代参数对象的值(也包括非 promise 值)。

失败/拒绝(Rejection):
如果传入的 promise 中有一个失败(rejected),Promise.all 异步地将失败的那个结果给失败状态的回调函数,而不管其它 promise 是否完成。

来个例子:

const promises = [
  Promise.resolve("a"),
  Promise.resolve("b"),
  Promise.resolve("c"),
];
Promise.all(promises)
  .then((arr) => assert.deepEqual(
    arr, ["a", "b", "c"]
  ));

如果其中的一个 promise 被拒绝,那么又是什么情况:

const promises = [
  Promise.resolve("a"),
  Promise.resolve("b"),
  Promise.reject("ERROR"),
];
Promise.all(promises)
  .catch((err) => assert.equal(
    err, "ERROR"
  ));

下图说明Promise.all()是如何工作的

4.1 异步 .map() 与 Promise.all()

数组转换方法,如.map().filter()等,用于同步计算。例如

function timesTwoSync(x) {
  return 2 * x;
}
const arr = [1, 2, 3];
const result = arr.map(timesTwoSync);
assert.deepEqual(result, [2, 4, 6]);

如果.map()的回调是基于Promise的函数会发生什么? 使用这种方式 .map()返回的的结果是一个Promises数组。

Promises数组不是普通代码可以使用的数据,但我们可以通过Promise.all()来解决这个问题:它将Promises数组转换为Promise,并使用一组普通值数组来实现。

function timesTwoAsync(x) {
  return new Promise(resolve => resolve(x * 2));
}
const arr = [1, 2, 3];
const promiseArr = arr.map(timesTwoAsync);
Promise.all(promiseArr)
  .then(result => {
    assert.deepEqual(result, [2, 4, 6]);
  });
更实际工作上关于 .map()示例

接下来,咱们使用.map()Promise.all()Web下载文件。 首先,咱们需要以下帮助函数:

function downloadText(url) {
  return fetch(url)
    .then((response) => { // (A)
      if (!response.ok) { // (B)
        throw new Error(response.statusText);
      }
      return response.text(); // (C)
    });
}

downloadText()使用基于Promise的fetch API 以字符串流的方式下载文件:

首先,它异步检索响应(第A行)。

response.ok(B行)检查是否存在“找不到文件”等错误。

如果没有错误,使用.text()(第C行)以字符串的形式取回文件的内容。

在下面的示例中,咱们 下载了两个文件

const urls = [
  "http://example.com/first.txt",
  "http://example.com/second.txt",
];

const promises = urls.map(
  url => downloadText(url));

Promise.all(promises)
  .then(
    (arr) => assert.deepEqual(
      arr, ["First!", "Second!"]
    ));

Promise.all()的一个简版实现
function all(iterable) {
  return new Promise((resolve, reject) => {
    let index = 0;
    for (const promise of iterable) {
      // Capture the current value of `index`
      const currentIndex = index;
      promise.then(
        (value) => {
          if (anErrorOccurred) return;
          result[currentIndex] = value;
          elementCount++;
          if (elementCount === result.length) {
            resolve(result);
          }
        },
        (err) => {
          if (anErrorOccurred) return;
          anErrorOccurred = true;
          reject(err);
        });
      index++;
    }
    if (index === 0) {
      resolve([]);
      return;
    }
    let elementCount = 0;
    let anErrorOccurred = false;
    const result = new Array(index);
  });
}
5. Promise.race()

Promise.race()方法的定义:

Promise.race(promises: Iterable>): Promise

Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。来几个例子,瞧瞧:

const promises = [
  new Promise((resolve, reject) =>
    setTimeout(() => resolve("result"), 100)), // (A)
  new Promise((resolve, reject) =>
    setTimeout(() => reject("ERROR"), 200)), // (B)
];
Promise.race(promises)
  .then((result) => assert.equal( // (C)
    result, "result"));

在第 A 行,Promise 是完成状态 ,所以 第 C 行会执行(尽管第 B 行被拒绝)。

如果 Promise 被拒绝首先执行,在来看看情况是嘛样的:

const promises = [
  new Promise((resolve, reject) =>
    setTimeout(() => resolve("result"), 200)),
  new Promise((resolve, reject) =>
    setTimeout(() => reject("ERROR"), 100)),
];
Promise.race(promises)
  .then(
    (result) => assert.fail(),
    (err) => assert.equal(
      err, "ERROR"));

注意,由于 Promse 先被拒绝,所以 Promise.race() 返回的是一个被拒绝的 Promise

这意味着Promise.race([])的结果永远不会完成。

下图演示了Promise.race()的工作原理:

Promise.race() 在 Promise 超时下的情况

在本节中,我们将使用Promise.race()来处理超时的 Promise。 以下辅助函数:

function resolveAfter(ms, value=undefined) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(value), ms);
  });
}

resolveAfter() 主要做的是在指定的时间内,返回一个状态为 resolvePromise,值为为传入的 value

调用上面方法:

function timeout(timeoutInMs, promise) {
  return Promise.race([
    promise,
    resolveAfter(timeoutInMs,
      Promise.reject(new Error("Operation timed out"))),
  ]);
}

timeout() 返回一个Promise,该 Promise 的状态取决于传入 promise 状态 。

其中 timeout 函数中的 resolveAfter(timeoutInMs, Promise.reject(new Error("Operation timed out")) ,通过 resolveAfter 定义可知,该结果返回的是一个被拒绝状态的 Promise

再来看看timeout(timeoutInMs, promise)的运行情况。如果传入promise在指定的时间之前状态为完成时,timeout 返回结果就是一个完成状态的 Promise,可以通过.then的第一个回调参数处理返回的结果。

timeout(200, resolveAfter(100, "Result!"))
  .then(result => assert.equal(result, "Result!"));

相反,如果是在指定的时间之后完成,刚 timeout 返回结果就是一个拒绝状态的 Promise,从而触发catch方法指定的回调函数。

timeout(100, resolveAfter(2000, "Result!"))
  .catch(err => assert.deepEqual(err, new Error("Operation timed out")));

重要的是要了解“Promise 超时”的真正含义:

如果传入入Promise 较到的得到解决,其结果就会给返回的 Promise

如果没有足够快得到解决,输出的 Promise 的状态为拒绝。

也就是说,超时只会阻止传入的Promise,影响输出 Promise(因为Promise只能解决一次), 但它并没有阻止传入Promise的异步操作。

5.2 Promise.race() 的一个简版实现

以下是 Promise.race()的一个简化实现(它不执行安全检查)

function race(iterable) {
  return new Promise((resolve, reject) => {
    for (const promise of iterable) {
      promise.then(
        (value) => {
          if (settlementOccurred) return;
          settlementOccurred = true;
          resolve(value);
        },
        (err) => {
          if (settlementOccurred) return;
          settlementOccurred = true;
          reject(err);
        });
    }
    let settlementOccurred = false;
  });
}
6.Promise.allSettled()

“Promise.allSettled”这一特性是由Jason WilliamsRobert PamelyMathias Bynens提出。

promise.allsettle()方法的定义:

Promise.allSettled(promises: Iterable<Promise>)
: Promise<Array>>

它返回一个ArrayPromise,其元素具有以下类型特征:

type SettlementObject = FulfillmentObject | RejectionObject;

interface FulfillmentObject {
  status: "fulfilled";
  value: T;
}

interface RejectionObject {
  status: "rejected";
  reason: unknown;
}

Promise.allSettled()方法返回一个promise,该promise在所有给定的promise已被解析或被拒绝后解析,并且每个对象都描述每个promise的结果。

举例说明, 比如各位用户在页面上面同时填了3个独立的表单, 这三个表单分三个接口提交到后端, 三个接口独立, 没有顺序依赖, 这个时候我们需要等到请求全部完成后给与用户提示表单提交的情况

在多个promise同时进行时咱们很快会想到使用Promise.all来进行包装, 但是由于Promise.all的短路特性, 三个提交中若前面任意一个提交失败, 则后面的表单也不会进行提交了, 这就与咱们需求不符合.

Promise.allSettledPromise.all类似, 其参数接受一个Promise的数组, 返回一个新的Promise, 唯一的不同在于, 其不会进行短路, 也就是说当Promise全部处理完成后我们可以拿到每个Promise的状态, 而不管其是否处理成功.

下图说明promise.allsettle()是如何工作的

6.1 Promise.allSettled() 例子

这是Promise.allSettled() 使用方式快速演示示例

Promise.allSettled([
  Promise.resolve("a"),
  Promise.reject("b"),
])
.then(arr => assert.deepEqual(arr, [
  { status: "fulfilled", value:  "a" },
  { status: "rejected",  reason: "b" },
]));
6.2 Promise.allSettled() 较复杂点的例子

这个示例类似于.map()Promise.all()示例(我们从其中借用了downloadText()函数):我们下载多个文本文件,这些文件的url存储在一个数组中。但是,这一次,咱们不希望在出现错误时停止,而是希望继续执行。Promise.allSettled()允许咱们这样做:

const urls = [
  "http://example.com/exists.txt",
  "http://example.com/missing.txt",
];

const result = Promise.allSettled(
  urls.map(u => downloadText(u)));
result.then(
  arr => assert.deepEqual(
    arr,
    [
      {
        status: "fulfilled",
        value: "Hello!",
      },
      {
        status: "rejected",
        reason: new Error("Not Found"),
      },
    ]
));
6.3 Promise.allSettled() 的简化实现

这是promise.allsettle()的简化实现(不执行安全检查)

function allSettled(iterable) {
  return new Promise((resolve, reject) => {
    function addElementToResult(i, elem) {
      result[i] = elem;
      elementCount++;
      if (elementCount === result.length) {
        resolve(result);
      }
    }

    let index = 0;
    for (const promise of iterable) {
      // Capture the current value of `index`
      const currentIndex = index;
      promise.then(
        (value) => addElementToResult(
          currentIndex, {
            status: "fulfilled",
            value
          }),
        (reason) => addElementToResult(
          currentIndex, {
            status: "rejected",
            reason
          }));
      index++;
    }
    if (index === 0) {
      resolve([]);
      return;
    }
    let elementCount = 0;
    const result = new Array(index);
  });
}
7. 短路特性

Promise.all()romise.race() 都具有 短路特性

Promise.all(): 如果参数中 promise 有一个失败(rejected),此实例回调失败(reject)

Promise.race():如果参数中某个promise解决或拒绝,返回的 promise就会解决或拒绝。

8.并发性和 Promise.all() 8.1 顺序执行与并发执行

考虑下面的代码:

asyncFunc1()
  .then(result1 => {
    assert.equal(result1, "one");
    return asyncFunc2();
  })
  .then(result2 => {
    assert.equal(result2, "two");
  });

使用.then()顺序执行基于Promise的函数:只有在 asyncFunc1()的结果被解决后才会执行asyncFunc2()

Promise.all() 是并发执行的

Promise.all([asyncFunc1(), asyncFunc2()])
  .then(arr => {
    assert.deepEqual(arr, ["one", "two"]);
  });
9.2 并发技巧:关注操作何时开始

确定并发异步代码的技巧:关注异步操作何时启动,而不是如何处理它们的Promises

例如,下面的每个函数都同时执行asyncFunc1()asyncFunc2(),因为它们几乎同时启动。

function concurrentAll() {
  return Promise.all([asyncFunc1(), asyncFunc2()]);
}

function concurrentThen() {
  const p1 = asyncFunc1();
  const p2 = asyncFunc2();
  return p1.then(r1 => p2.then(r2 => [r1, r2]));
}

另一方面,以下两个函数依次执行asyncFunc1()asyncFunc2(): asyncFunc2()仅在asyncFunc1()的解决之后才调用。

function sequentialThen() {
  return asyncFunc1()
    .then(r1 => asyncFunc2()
      .then(r2 => [r1, r2]));
}

function sequentialAll() {
  const p1 = asyncFunc1();
  const p2 = p1.then(() => asyncFunc2());
  return Promise.all([p1, p2]);
}

9.3 Promise.all() 与 Fork-Join 分治编程

Promise.all() 与并发模式“fork join”松散相关。重温一下咱们前面的一个例子:

Promise.all([
    // (A) fork
    downloadText("http://example.com/first.txt"),
    downloadText("http://example.com/second.txt"),
  ])
  // (B) join
  .then(
    (arr) => assert.deepEqual(
      arr, ["First!", "Second!"]
    ));

Fork:在A行中,分割两个异步任务并同时执行它们。

Join:在B行中,对每个小任务得到的结果进行汇总。

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/106730.html

相关文章

  • 深度理解Promise--Promise的特点和方法详解

    摘要:实例生成以后,用方法分别指定状态和状态的回调函数。则是或的别名,用于指定发生错误时的回调函数。上述代码也可以理解成这样处理和前一个回调函数运行时发生的错误发生错误方法用于指定不管对象最后状态如何,都会执行的回调函数。 什么是promise? Promise(承诺),在程序中的意思就是承诺我过一段时间(通常是一个异步操作)后会给你一个结果,是异步编程的一种解决方案。从语法上说,原生Pro...

    wqj97 评论0 收藏0
  • 精读《What's new in javascript》

    摘要:举例来说即便某个失败了,也不会导致的发生,这样在不在乎是否有项目失败,只要拿到都结束的信号的场景很有用。对于则稍有不同只要有子项,就会完成,哪怕第一个了,而第二个了,也会,而对于,这种场景会直接。 1. 引言 本周精读的内容是:Google I/O 19。 2019 年 Google I/O 介绍了一些激动人心的 JS 新特性,这些特性有些已经被主流浏览器实现,并支持 polyfill...

    dabai 评论0 收藏0
  • JS引擎:它们是如何工作的?从调用堆栈到Promise,需要知道的所有内容

    摘要:最受欢迎的引擎是,由和使用,用于,以及使用的。引擎它们是如何工作的全局执行上下文和调用堆栈刚刚了解了引擎如何读取变量和函数声明,它们最终被放入了全局内存堆中。事件循环只有一个任务它检查调用堆栈是否为空。 为了保证可读性,本文采用意译而非直译。 想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你! 有没有想过浏览器如何读取和运行JS代码? 这看起来很神奇,我们可以通过浏览...

    lavnFan 评论0 收藏0
  • JavaScript引擎是如何工作的?从调用栈到Promise你需要知道的一切

    摘要:最受欢迎的引擎是,在和中使用,用于,以及所使用的。怎么处理每个引擎都有一个基本组件,称为调用栈。也就是说,如果有其他函数等待执行,函数是不能离开调用栈的。每个异步函数在被送入调用栈之前必须通过回调队列。例如方法是在中传递的回调函数。   翻译:疯狂的技术宅 原文:www.valentinog.com/blog/engine… 从Call Stack,Global Me...

    zzbo 评论0 收藏0
  • JavaScript引擎是如何工作的?从调用栈到Promise你需要知道的一切

    摘要:最受欢迎的引擎是,在和中使用,用于,以及所使用的。单线程的我们说是单线程的,因为有一个调用栈处理我们的函数。也就是说,如果有其他函数等待执行,函数是不能离开调用栈的。每个异步函数在被送入调用栈之前必须通过回调队列。 翻译:疯狂的技术宅原文:https://www.valentinog.com/bl... 本文首发微信公众号:前端先锋欢迎关注,每天都给你推送新鲜的前端技术文章 sh...

    Simon_Zhou 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<