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

更新日期: 2020-02-13阅读: 1.7k标签: Promise

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


1.概述

Promise.all<T>(promises: Iterable<Promise<T>>): Promise<Array<T>>

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

Promise.race<T>(promises: Iterable<Promise<T>>): Promise<T>

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

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

  • 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<T>(promises: Iterable<Promise<T>>): Promise<Array<T>>

返回情况:

完成(Fulfillment):
如果传入的可迭代对象为空,Promise.all 会同步地返回一个已完成(resolved)状态的promise。
如果所有传入的 promise 都变为完成状态,或者传入的可迭代对象内没有 promise,Promise.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<T>(promises: Iterable<Promise<T>>): Promise<T>

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() 主要做的是在指定的时间内,返回一个状态为 resolve 的 Promise,值为为传入的 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 超时”的真正含义:

  1. 如果传入入Promise 较到的得到解决,其结果就会给返回的 Promise。
  2. 如果没有足够快得到解决,输出的 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<T>(promises: Iterable<Promise<T>>)
    Promise<Array<SettlementObject<T>>>

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

type SettlementObject<T> = FulfillmentObject<T> | RejectionObject;

interface FulfillmentObject<T> {
  status: 'fulfilled';
  value: T;
}

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

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

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

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

Promise.allSettled跟Promise.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']);
  });

8.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]);
}

8.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行中,对每个小任务得到的结果进行汇总。


链接: https://fly63.com/article/detial/8179

你真的了解 Promise 吗?Promise 必知必会(十道题)

Promise 想必大家十分熟悉,想想就那么几个 api,可是你真的了解 Promise 吗?本文根据 Promise 的一些知识点总结了十道题,看看你能做对几道。

剖析Promise内部结构,一步一步实现一个完整的、能通过所有Test case的Promise类

本文写给有一定Promise使用经验的人,如果你还没有使用过Promise,这篇文章可能不适合你,Promise标准中仅指定了Promise对象的then方法的行为,其它一切我们常见的方法/函数都并没有指定.

Async/Await替代Promise的6个理由

Async/Await替代Promise的6个理由:Async/Await是近年来JavaScript添加的最革命性的的特性之一。它会让你发现Promise的语法有多糟糕,而且提供了一个直观的替代方法。

Promise 原理解析与实现(遵循Promise/A+规范)

Promise是JS异步编程中的重要概念,异步抽象处理对象,是目前比较流行Javascript异步编程解决方案之一,Promise 是一个构造函数, new Promise 返回一个 promise对象 接收一个excutor执行函数作为参数

简单模仿实现 Promise 的异步模式

这篇文章是考虑如何自己实现一个简单 Promise,用以理解 Promise。和原生 Promise的调用方法一样,支持链式调用,本文实现的方法只能用于参考Promise的原理,还有很多特性没有实现,比如 race,all 方法的实现。

数组的遍历你都会用了,那Promise版本的呢

在对数组进行一些遍历操作时,发现有些遍历方法对Promise的反馈并不是我们想要的结果。async/await为Promise的语法糖,文中会直接使用async/await替换Promise;map可以说是对Promise最友好的一个函数了,

Promise使用时应注意的问题

最近在使用axios库时遇到了个问题,后端接口报了500错误,但前端并未捕获到。在axios整体配置的代码中,过滤http code时,调用了filter401()、filter500(),但是这里注意并未将两个filter函数的结果返回,也就是并未返回promise,这就是导致问题出现的原因

es6 Promise 的基础用法

想必接触过Node的人都知道,Node是以异步(Async)回调著称的,其异步性提高了程序的执行效率,但同时也减少了程序的可读性。如果我们有几个异步操作,并且后一个操作需要前一个操作返回的数据才能执行

关于 Promise 的 9 个提示

你可以在 .then 里面 return 一个 Promise,每次执行 .then 的时候都会自动创建一个新的 Promise,对调用者来说,Promise 的 resolved/rejected 状态是唯一的,Promise 构造函数不是解决方案,使用 Promise.resolve

手写一款符合Promise/A+规范的Promise

Promise的一些用法在此不多赘述,本篇主要带领你手写一个Promise源码,学完你就会发现:Promise没有你想象中的那么难.本篇大概分为以下步骤:实现简单的同步Promise、增加异步功能、增加链式调用then、增加catch finally方法、增加all race 等方法、实现一个promise的延迟对象defer、最终测试

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!