面向急性子的程序员的 JavaScript(ES2022 版)
请支持本书:购买捐赠
(广告,请勿屏蔽。)

40 异步编程的 Promise [ES6]



  推荐阅读

本章建立在前一章的基础上,介绍了 JavaScript 中的异步编程。

40.1 使用 Promise 的基础知识

Promise 是一种用于异步传递结果的技术。

40.1.1 使用基于 Promise 的函数

以下代码是使用基于 Promise 的函数 addAsync() 的示例(其实现将在稍后展示)

addAsync(3, 4)
  .then(result => { // success
    assert.equal(result, 7);
  })
  .catch(error => { // failure
    assert.fail(error);
  });

Promise 类似于事件模式:有一个对象(一个 *Promise*),我们在其中注册回调

基于 Promise 的函数返回一个 Promise,并在完成时(如果完成)向其发送结果或错误。Promise 将其传递给相关的回调。

与事件模式相比,Promise 针对一次性结果进行了优化

40.1.2 什么是 Promise?

什么是 Promise?有两种看待它的方式

40.1.3 实现基于 Promise 的函数

这是一个基于 Promise 的函数的实现,它将两个数字 xy 相加

function addAsync(x, y) {
  return new Promise(
    (resolve, reject) => { // (A)
      if (x === undefined || y === undefined) {
        reject(new Error('Must provide two parameters'));
      } else {
        resolve(x + y);
      }
    });
}

addAsync() 立即调用 Promise 构造函数。该函数的实际实现位于传递给该构造函数的回调中(A 行)。该回调提供了两个函数

40.1.4 Promise 的状态

Figure 22: A Promise can be in either one of three states: pending, fulfilled, or rejected. If a Promise is in a final (non-pending) state, it is called settled.

22 描述了 Promise 可以处于的三种状态。Promise 专注于一次性结果,并保护我们免受 *竞争条件*(注册过早或过晚)的影响

此外,一旦 Promise 完成,其状态和完成值就不能再更改。这有助于使代码可预测并强制执行 Promise 的一次性性质。

  有些 Promise 永远不会完成

Promise 可能永远不会完成。例如

new Promise(() => {})

40.1.5 Promise.resolve():创建一个以给定值完成的 Promise

Promise.resolve(x) 创建一个以值 x 完成的 Promise

Promise.resolve(123)
  .then(x => {
    assert.equal(x, 123);
  });

如果参数已经是一个 Promise,则按原样返回

const abcPromise = Promise.resolve('abc');
assert.equal(
  Promise.resolve(abcPromise),
  abcPromise);

因此,给定一个任意值 x,我们可以使用 Promise.resolve(x) 来确保我们有一个 Promise。

请注意,名称是 resolve,而不是 fulfill,因为如果 .resolve() 的参数是一个被拒绝的 Promise,它将返回一个被拒绝的 Promise。

40.1.6 Promise.reject():创建一个以给定值拒绝的 Promise

Promise.reject(err) 创建一个以值 err 拒绝的 Promise

const myError = new Error('My error!');
Promise.reject(myError)
  .catch(err => {
    assert.equal(err, myError);
  });

40.1.7 在 .then() 回调中返回和抛出

.then() 处理 Promise 的完成。它还返回一个新的 Promise。该 Promise 如何完成取决于回调内部发生的情况。让我们来看看三种常见情况。

40.1.7.1 返回非 Promise 值

首先,回调可以返回一个非 Promise 值(A 行)。因此,.then() 返回的 Promise 将以该值完成(如 B 行中所检查)

Promise.resolve('abc')
  .then(str => {
    return str + str; // (A)
  })
  .then(str2 => {
    assert.equal(str2, 'abcabc'); // (B)
  });
40.1.7.2 返回 Promise

其次,回调可以返回一个 Promise p(A 行)。因此,p“成为”.then() 返回的内容。换句话说:.then() 已经返回的 Promise 被有效地替换为 p

Promise.resolve('abc')
  .then(str => {
    return Promise.resolve(123); // (A)
  })
  .then(num => {
    assert.equal(num, 123);
  });

为什么这很有用?我们可以返回基于 Promise 的操作的结果,并通过“扁平的”(非嵌套的).then() 处理其完成值。比较

// Flat
asyncFunc1()
  .then(result1 => {
    /*···*/
    return asyncFunc2();
  })
  .then(result2 => {
    /*···*/
  });

// Nested
asyncFunc1()
  .then(result1 => {
    /*···*/
    asyncFunc2()
    .then(result2 => {
      /*···*/
    });
  });
40.1.7.3 抛出异常

第三,回调可以抛出异常。因此,.then() 返回的 Promise 将以该异常被拒绝。也就是说,同步错误被转换为异步错误。

const myError = new Error('My error!');
Promise.resolve('abc')
  .then(str => {
    throw myError;
  })
  .catch(err => {
    assert.equal(err, myError);
  });

40.1.8 .catch() 及其回调

.then().catch() 之间的区别在于,后者由拒绝触发,而不是由完成触发。但是,这两种方法都以相同的方式将回调的操作转换为 Promise。例如,在以下代码中,A 行中 .catch() 回调返回的值成为完成值

const err = new Error();

Promise.reject(err)
  .catch(e => {
    assert.equal(e, err);
    // Something went wrong, use a default value
    return 'default value'; // (A)
  })
  .then(str => {
    assert.equal(str, 'default value');
  });

40.1.9 链接方法调用

.then().catch() 始终返回 Promise。这使我们能够创建任意长的方法调用链

function myAsyncFunc() {
  return asyncFunc1() // (A)
    .then(result1 => {
      // ···
      return asyncFunc2(); // a Promise
    })
    .then(result2 => {
      // ···
      return result2 ?? '(Empty)'; // not a Promise
    })
    .then(result3 => {
      // ···
      return asyncFunc4(); // a Promise
    });
}

由于链接,A 行中的 return 返回最后一个 .then() 的结果。

在某种程度上,.then() 是同步分号的异步版本

我们还可以将 .catch() 添加到其中,并让它同时处理多个错误源

asyncFunc1()
  .then(result1 => {
    // ···
    return asyncFunction2();
  })
  .then(result2 => {
    // ···
  })
  .catch(error => {
    // Failure: handle errors of asyncFunc1(), asyncFunc2()
    // and any (sync) exceptions thrown in previous callbacks
  });

40.1.10 .finally() [ES2018]

Promise 方法 .finally() 通常按如下方式使用

somePromise
  .then((result) => {
    // ···
  })
  .catch((error) => {
    // ···
  })
  .finally(() => {
    // ···
  })
;

.finally() 回调始终执行,而与 somePromise 以及 .then() 和/或 .catch() 返回的值无关。相反

.finally() 忽略其回调返回的内容,并简单地传递调用它之前存在的完成状态

Promise.resolve(123)
  .finally(() => {})
  .then((result) => {
    assert.equal(result, 123);
  });

Promise.reject('error')
  .finally(() => {})
  .catch((error) => {
    assert.equal(error, 'error');
  });

但是,如果 .finally() 回调抛出异常,则 .finally() 返回的 Promise 将被拒绝

Promise.reject('error (originally)')
  .finally(() => {
    throw 'error (finally)';
  })
  .catch((error) => {
    assert.equal(error, 'error (finally)');
  });
40.1.10.1 .finally() 的用例:清理

.finally() 的一个常见用例类似于同步 finally 子句的常见用例:在使用完资源后进行清理。无论一切顺利还是出现错误,都应该始终这样做,例如

let connection;
db.open()
.then((conn) => {
  connection = conn;
  return connection.select({ name: 'Jane' });
})
.then((result) => {
  // Process result
  // Use `connection` to make more queries
})
// ···
.catch((error) => {
  // handle errors
})
.finally(() => {
  connection.close();
});
40.1.10.2 .finally() 的用例:在任何类型的完成之后首先执行某些操作

我们还可以在 .then().catch() 之前使用 .finally()。然后,我们在 .finally() 回调中执行的操作始终在其他两个回调之前执行。

例如,这就是完成的 Promise 会发生的情况

Promise.resolve('fulfilled')
  .finally(() => {
    console.log('finally');
  })
  .then((result) => {
    console.log('then ' + result);
  })
  .catch((error) => {
    console.log('catch ' + error);
  })
;
// Output:
// 'finally'
// 'then fulfilled'

这就是被拒绝的 Promise 会发生的情况

Promise.reject('rejected')
  .finally(() => {
    console.log('finally');
  })
  .then((result) => {
    console.log('then ' + result);
  })
  .catch((error) => {
    console.log('catch ' + error);
  })
;
// Output:
// 'finally'
// 'catch rejected'

40.1.11 Promise 相对于普通回调的优势

在处理一次性结果时,Promise 相对于普通回调具有一些优势

Promise 的最大优势之一是不需要直接使用它们:它们是 *async 函数* 的基础,async 函数是一种用于执行异步计算的同步外观语法。异步函数将在下一章中介绍。

40.2 示例

查看 Promise 的实际应用有助于理解它们。让我们看一些例子。

40.2.1 Node.js:异步读取文件

考虑以下文本文件 person.json,其中包含 JSON 数据

{
  "first": "Jane",
  "last": "Doe"
}

让我们来看看两个版本的代码,它们读取这个文件并将其解析成一个对象。首先,基于回调的版本。其次,基于 Promise 的版本。

40.2.1.1 基于回调的版本

以下代码读取该文件的内容并将其转换为 JavaScript 对象。它基于 Node.js 风格的回调

import * as fs from 'fs';
fs.readFile('person.json',
  (error, text) => {
    if (error) { // (A)
      // Failure
      assert.fail(error);
    } else {
      // Success
      try { // (B)
        const obj = JSON.parse(text); // (C)
        assert.deepEqual(obj, {
          first: 'Jane',
          last: 'Doe',
        });
      } catch (e) {
        // Invalid JSON
        assert.fail(e);
      }
    }
  });

fs 是一个内置的 Node.js 模块,用于文件系统操作。我们使用基于回调的函数 fs.readFile() 来读取名为 person.json 的文件。如果我们成功了,内容将通过参数 text 作为字符串传递。在 C 行,我们将该字符串从基于文本的数据格式 JSON 转换为 JavaScript 对象。JSON 是一个具有用于消费和生成 JSON 的方法的对象。它是 JavaScript 标准库的一部分,并在 本书后面 有详细介绍。

请注意,这里有两种错误处理机制:A 行中的 if 处理 fs.readFile() 报告的异步错误,而 B 行中的 try 处理 JSON.parse() 报告的同步错误。

40.2.1.2 基于 Promise 的版本

以下代码使用 readFileAsync(),它是 fs.readFile() 的基于 Promise 的版本(通过 util.promisify() 创建,稍后解释)

readFileAsync('person.json')
  .then(text => { // (A)
    // Success
    const obj = JSON.parse(text);
    assert.deepEqual(obj, {
      first: 'Jane',
      last: 'Doe',
    });
  })
  .catch(err => { // (B)
    // Failure: file I/O error or JSON syntax error
    assert.fail(err);
  });

函数 readFileAsync() 返回一个 Promise。在 A 行,我们通过该 Promise 的 .then() 方法指定了一个成功回调。then 回调中的其余代码是同步的。

.then() 返回一个 Promise,它允许在 B 行调用 Promise 方法 .catch()。我们使用它来指定一个失败回调。

请注意,.catch() 允许我们同时处理 readFileAsync() 的异步错误和 JSON.parse() 的同步错误,因为 .then() 回调内部的异常会变成拒绝。

40.2.2 浏览器:Promise 化 XMLHttpRequest

我们之前已经看到了用于在 Web 浏览器中下载数据的基于事件的 XMLHttpRequest API。以下函数对该 API 进行了 Promise 化

function httpGet(url) {
  return new Promise(
    (resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.onload = () => {
        if (xhr.status === 200) {
          resolve(xhr.responseText); // (A)
        } else {
          // Something went wrong (404, etc.)
          reject(new Error(xhr.statusText)); // (B)
        }
      }
      xhr.onerror = () => {
        reject(new Error('Network error')); // (C)
      };
      xhr.open('GET', url);
      xhr.send();
    });
}

请注意 XMLHttpRequest 的结果和错误是如何通过 resolve()reject() 处理的

以下是使用 httpGet() 的方法

httpGet('http://example.com/textfile.txt')
  .then(content => {
    assert.equal(content, 'Content of textfile.txt\n');
  })
  .catch(error => {
    assert.fail(error);
  });

  练习:Promise 超时

exercises/promises/promise_timeout_test.mjs

40.2.3 Node.js:util.promisify()

util.promisify() 是一个实用函数,它将基于回调的函数 f 转换为基于 Promise 的函数。也就是说,我们正在从这种类型签名

f(arg_1, ···, arg_n, (err: Error, result: T) => void) : void

转换为这种类型签名

f(arg_1, ···, arg_n) : Promise<T>

以下代码对基于回调的 fs.readFile() 进行了 Promise 化(A 行)并使用它

import * as fs from 'fs';
import {promisify} from 'util';

const readFileAsync = promisify(fs.readFile); // (A)

readFileAsync('some-file.txt', {encoding: 'utf8'})
  .then(text => {
    assert.equal(text, 'The content of some-file.txt\n');
  })
  .catch(err => {
    assert.fail(err);
  });

  练习:util.promisify()

40.2.4 浏览器:Fetch API

所有现代浏览器都支持 Fetch,这是一种新的基于 Promise 的 API,用于下载数据。可以将其视为 XMLHttpRequest 的基于 Promise 的版本。以下是 该 API 的摘录

interface Body {
  text() : Promise<string>;
  ···
}
interface Response extends Body {
  ···
}
declare function fetch(str) : Promise<Response>;

这意味着我们可以像这样使用 fetch()

fetch('http://example.com/textfile.txt')
  .then(response => response.text())
  .then(text => {
    assert.equal(text, 'Content of textfile.txt\n');
  });

  练习:使用 fetch API

exercises/promises/fetch_json_test.mjs

40.3 错误处理:不要混淆拒绝和异常

实现函数和方法的规则

不要混淆(异步)拒绝和(同步)异常。

这使得我们的同步和异步代码更具可预测性和更简单,因为我们始终可以专注于单一错误处理机制。

对于基于 Promise 的函数和方法,该规则意味着它们永远不应该抛出异常。唉,很容易不小心就弄错了——例如

// Don’t do this
function asyncFunc() {
  doSomethingSync(); // (A)
  return doSomethingAsync()
    .then(result => {
      // ···
    });
}

问题是,如果在 A 行抛出异常,那么 asyncFunc() 将抛出一个异常。该函数的调用者只期望拒绝,而没有为异常做好准备。我们可以通过三种方法来解决这个问题。

我们可以将函数的整个主体包装在一个 try-catch 语句中,并在抛出异常时返回一个被拒绝的 Promise

// Solution 1
function asyncFunc() {
  try {
    doSomethingSync();
    return doSomethingAsync()
      .then(result => {
        // ···
      });
  } catch (err) {
    return Promise.reject(err);
  }
}

鉴于 .then() 会将异常转换为拒绝,我们可以在 .then() 回调内部执行 doSomethingSync()。为此,我们通过 Promise.resolve() 启动一个 Promise 链。我们忽略了初始 Promise 的完成值 undefined

// Solution 2
function asyncFunc() {
  return Promise.resolve()
    .then(() => {
      doSomethingSync();
      return doSomethingAsync();
    })
    .then(result => {
      // ···
    });
}

最后,new Promise() 也会将异常转换为拒绝。因此,使用此构造函数类似于前面的解决方案

// Solution 3
function asyncFunc() {
  return new Promise((resolve, reject) => {
      doSomethingSync();
      resolve(doSomethingAsync());
    })
    .then(result => {
      // ···
    });
}

40.4 基于 Promise 的函数同步开始,异步结束

大多数基于 Promise 的函数都按如下方式执行

以下代码演示了这一点

function asyncFunc() {
  console.log('asyncFunc');
  return new Promise(
    (resolve, _reject) => {
      console.log('new Promise()');
      resolve();
    });
}
console.log('START');
asyncFunc()
  .then(() => {
    console.log('.then()'); // (A)
  });
console.log('END');

// Output:
// 'START'
// 'asyncFunc'
// 'new Promise()'
// 'END'
// '.then()'

我们可以看到,new Promise() 的回调在代码结束之前执行,而结果在稍后传递(A 行)。

这种方法的好处

  有关此方法的更多信息

“为异步设计 API”,作者:Isaac Z. Schlueter

40.5 Promise 组合器函数:使用 Promise 数组

40.5.1 什么是 Promise 组合器函数?

组合器模式 是函数式编程中用于构建结构的一种模式。它基于两种函数

当涉及到 JavaScript Promise 时

接下来,我们将仔细研究一下提到的 Promise 组合器。

40.5.2 Promise.all()

这是 Promise.all() 的类型签名

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

Promise.all() 返回一个 Promise,它

这是一个输出 Promise 被完成的快速演示

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

以下示例演示了如果至少有一个输入 Promise 被拒绝会发生什么

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

图 23 说明了 Promise.all() 的工作原理。

Figure 23: The Promise combinator Promise.all().
40.5.2.1 通过 Promise.all() 实现异步 .map()

数组转换方法(如 .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 的函数(将普通值映射到 Promise 的函数),会发生什么?那么 .map() 的结果是一个 Promise 数组。唉,这不是普通代码可以处理的数据。值得庆幸的是,我们可以通过 Promise.all() 来解决这个问题:它将 Promise 数组转换为一个 Promise,该 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]);
  });
40.5.2.2 更真实的 .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 将文本文件下载为字符串

在以下示例中,我们下载了两个文本文件

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!']
    ));
40.5.2.3 Promise.all() 的简单实现

这是 Promise.all() 的简化实现(例如,它不执行安全检查)

function all(iterable) {
  return new Promise((resolve, reject) => {
    let elementCount = 0;
    let result;

    let index = 0;
    for (const promise of iterable) {
      // Preserve the current value of `index`
      const currentIndex = index;
      promise.then(
        (value) => {
          result[currentIndex] = value;
          elementCount++;
          if (elementCount === result.length) {
            resolve(result); // (A)
          }
        },
        (err) => {
          reject(err); // (B)
        });
      index++;
    }
    if (index === 0) {
      resolve([]);
      return;
    }
    // Now we know how many Promises there are in `iterable`.
    // We can wait until now with initializing `result` because
    // the callbacks of .then() are executed asynchronously.
    result = new Array(index);
  });
}

结果 Promise 被解决的两个主要位置是 A 行和 B 行。其中一个解决后,另一个就不能再更改解决值了,因为一个 Promise 只能被解决一次。

40.5.3 Promise.race()

这是 Promise.race() 的类型签名

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

Promise.race() 返回一个 Promise q,一旦 promises 中的第一个 Promise p 被解决,它就会被解决。q 具有与 p 相同的解决值。

在以下演示中,已完成的 Promise 的解决(A 行)发生在被拒绝的 Promise 的解决(B 行)之前。因此,结果也被完成了(C 行)。

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'));

在下一个演示中,拒绝先发生

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'));

请注意,Promise.race() 返回的 Promise 会在其输入 Promise 中的第一个 Promise 被解决后立即解决。这意味着 Promise.race([]) 的结果永远不会被解决。

图 24 说明了 Promise.race() 的工作原理。

Figure 24: The Promise combinator Promise.race().
40.5.3.1 使用 Promise.race() 使 Promise 超时

在本节中,我们将使用 Promise.race() 使 Promise 超时。以下辅助函数将在多次使用时很有用

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

resolveAfter() 返回一个 Promise,该 Promise 在 ms 毫秒后使用 value 解决。

此函数使 Promise 超时

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

timeout() 返回一个 Promise,其解决与以下两个 Promise 中先解决的那个相同

  1. 参数 promise
  2. timeoutInMs 毫秒后被拒绝的 Promise

为了生成第二个 Promise,timeout() 利用了这样一个事实,即使用被拒绝的 Promise 解决一个待定的 Promise 会导致前者被拒绝。

让我们看看 timeout() 的实际效果。在这里,输入 Promise 在超时之前就已完成。因此,输出 Promise 也已完成。

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

在这里,超时发生在输入 Promise 完成之前。因此,输出 Promise 被拒绝。

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

重要的是要理解“使 Promise 超时”的真正含义

也就是说,超时只会阻止输入 Promise 影响输出(因为一个 Promise 只能被解决一次)。但它不会停止生成输入 Promise 的异步操作。

40.5.3.2 Promise.race() 的简单实现

这是 Promise.race() 的简化实现(例如,它不执行安全检查)

function race(iterable) {
  return new Promise((resolve, reject) => {
    for (const promise of iterable) {
      promise.then(
        (value) => {
          resolve(value); // (A)
        },
        (err) => {
          reject(err); // (B)
        });
    }
  });
}

结果 Promise 在 A 行或 B 行被解决。一旦被解决,解决值就不能再更改了。

40.5.4 Promise.any()AggregateError [ES2021]

这是 Promise.any() 的类型签名

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

Promise.any() 返回一个 Promise p。它的解决方式取决于参数 promises(它指的是一个 Promise 的可迭代对象)

这是 AggregateErrorError 的子类)的类型签名

class AggregateError extends Error {
  // Instance properties (complementing the ones of Error)
  errors: Array<any>;

  constructor(
    errors: Iterable<any>,
    message: string = '',
    options?: ErrorOptions // ES2022
  );
}
interface ErrorOptions {
  cause?: any; // ES2022
}

图 25 展示了 Promise.any() 的工作原理。

Figure 25: The Promise combinator Promise.any().
40.5.4.1 两个首要示例

如果一个 Promise 成功解决,就会发生这种情况

const promises = [
  Promise.reject('ERROR A'),
  Promise.reject('ERROR B'),
  Promise.resolve('result'),
];
Promise.any(promises)
  .then((result) => assert.equal(
    result, 'result'
  ));

如果所有 Promise 都被拒绝,就会发生这种情况

const promises = [
  Promise.reject('ERROR A'),
  Promise.reject('ERROR B'),
  Promise.reject('ERROR C'),
];
Promise.any(promises)
  .catch((aggregateError) => assert.deepEqual(
    aggregateError.errors,
    ['ERROR A', 'ERROR B', 'ERROR C']
  ));
40.5.4.2 Promise.any()Promise.all() 的比较

Promise.any()Promise.all() 可以通过两种方式进行比较

40.5.4.3 Promise.any()Promise.race() 的比较

Promise.any()Promise.race() 也有关联,但关注的是不同的事情

.race() 的主要(相对罕见)用例是 Promise 超时。.any() 的用例更广泛。我们接下来会看看。

40.5.4.4 Promise.any() 的用例

如果我们有多个异步计算,并且我们只对第一个成功的计算感兴趣,则使用 Promise.any()。在某种程度上,我们让计算相互竞争,并使用最快的那个。

以下代码演示了下载资源时的样子

const resource = await Promise.any([
  fetch('http://example.com/first.txt')
    .then(response => response.text()),
  fetch('http://example.com/second.txt')
    .then(response => response.text()),
]);

相同的模式使我们能够使用下载速度更快的任何模块

const lodash = await Promise.any([
  import('https://primary.example.com/lodash'),
  import('https://secondary.example.com/lodash'),
]);

为了进行比较,如果辅助服务器只是一个备用服务器(以防主服务器出现故障),我们会使用以下代码

let lodash;
try {
  lodash = await import('https://primary.example.com/lodash');
} catch {
  lodash = await import('https://secondary.example.com/lodash');
}
40.5.4.5 我们如何实现 Promise.any()

Promise.any() 的简单实现基本上是 Promise.all() 实现的镜像版本。

40.5.5 Promise.allSettled() [ES2020]

这一次,类型签名稍微复杂一些。您可以随意跳到第一个演示,它应该更容易理解。

这是 Promise.allSettled() 的类型签名

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

它返回一个数组的 Promise,该数组的元素具有以下类型签名

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

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

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

Promise.allSettled() 返回一个 Promise out。一旦所有 promises 都已得到结果,out 就会使用一个数组得到解决。该数组的每个元素 e 对应于 promises 的一个 Promise p

除非在迭代 promises 时出错,否则输出 Promise out 永远不会被拒绝。

图 26 展示了 Promise.allSettled() 的工作原理。

Figure 26: The Promise combinator Promise.allSettled().
40.5.5.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' },
]));
40.5.5.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'),
      },
    ]
));
40.5.5.3 Promise.allSettled() 的简单实现

这是 Promise.allSettled() 的简化实现(例如,它不执行安全检查)

function allSettled(iterable) {
  return new Promise((resolve, reject) => {
    let elementCount = 0;
    let result;

    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;
    }
    // Now we know how many Promises there are in `iterable`.
    // We can wait until now with initializing `result` because
    // the callbacks of .then() are executed asynchronously.
    result = new Array(index);
  });
}

40.5.6 短路(高级)

对于 Promise 组合器,*短路*意味着输出 Promise 会提前得到结果 - 在所有输入 Promise 都得到结果之前。以下组合器会短路

再次强调,提前得到结果并不意味着被忽略的 Promise 背后的操作会停止。它只是意味着它们的解决被忽略了。

40.6 并发和 Promise.all()(高级)

40.6.1 顺序执行与并发执行

考虑以下代码

const asyncFunc1 = () => Promise.resolve('one');
const asyncFunc2 = () => Promise.resolve('two');

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

以这种方式使用 .then() 会*顺序*执行基于 Promise 的函数:只有在 asyncFunc1() 的结果得到结果后,才会执行 asyncFunc2()

Promise.all() 帮助更并发地执行基于 Promise 的函数

Promise.all([asyncFunc1(), asyncFunc2()])
  .then(arr => {
    assert.deepEqual(arr, ['one', 'two']);
  });

40.6.2 并发提示:关注操作何时开始

确定异步代码“并发”程度的提示:关注异步操作何时开始,而不是它们的 Promise 如何处理。

例如,以下每个函数都并发执行 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() 的 Promise 得到解决后才被调用。

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

40.6.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!']
    ));

40.7 链接 Promise 的技巧

本节提供链接 Promise 的技巧。

40.7.1 链接错误:丢失尾部

问题

// Don’t do this
function foo() {
  const promise = asyncFunc();
  promise.then(result => {
    // ···
  });

  return promise;
}

计算从 asyncFunc() 返回的 Promise 开始。但之后,计算继续,并通过 .then() 创建另一个 Promise。foo() 返回前一个 Promise,但应该返回后一个 Promise。解决方法如下

function foo() {
  const promise = asyncFunc();
  return promise.then(result => {
    // ···
  });
}

40.7.2 链接错误:嵌套

问题

// Don’t do this
asyncFunc1()
  .then(result1 => {
    return asyncFunc2()
    .then(result2 => { // (A)
      // ···
    });
  });

A 行中的 .then() 是嵌套的。扁平结构会更好

asyncFunc1()
  .then(result1 => {
    return asyncFunc2();
  })
  .then(result2 => {
    // ···
  });

40.7.3 链接错误:不必要的嵌套

这是另一个可以避免的嵌套示例

// Don’t do this
asyncFunc1()
  .then(result1 => {
    if (result1 < 0) {
      return asyncFuncA()
      .then(resultA => 'Result: ' + resultA);
    } else {
      return asyncFuncB()
      .then(resultB => 'Result: ' + resultB);
    }
  });

我们可以再次获得扁平结构

asyncFunc1()
  .then(result1 => {
    return result1 < 0 ? asyncFuncA() : asyncFuncB();
  })
  .then(resultAB => {
    return 'Result: ' + resultAB;
  });

40.7.4 并非所有嵌套都是不好的

在以下代码中,我们实际上受益于嵌套

db.open()
  .then(connection => { // (A)
    return connection.select({ name: 'Jane' })
      .then(result => { // (B)
        // Process result
        // Use `connection` to make more queries
      })
      // ···
      .finally(() => {
        connection.close(); // (C)
      });
  })

我们在 A 行中接收异步结果。在 B 行中,我们进行嵌套,以便我们可以在回调内部和 C 行中访问变量 connection

40.7.5 链接错误:创建 Promise 而不是链接

问题

// Don’t do this
class Model {
  insertInto(db) {
    return new Promise((resolve, reject) => { // (A)
      db.insert(this.fields)
        .then(resultCode => {
          this.notifyObservers({event: 'created', model: this});
          resolve(resultCode);
        }).catch(err => {
          reject(err);
        })
    });
  }
  // ···
}

在 A 行中,我们正在创建一个 Promise 来传递 db.insert() 的结果。这过于冗长,可以简化

class Model {
  insertInto(db) {
    return db.insert(this.fields)
      .then(resultCode => {
        this.notifyObservers({event: 'created', model: this});
        return resultCode;
      });
  }
  // ···
}

关键是我们不需要创建一个 Promise;我们可以返回 .then() 调用的结果。另一个好处是我们不需要捕获并重新拒绝 db.insert() 的失败。我们只需将其拒绝传递给 .insertInto() 的调用者。

40.8 快速参考:Promise 组合器函数

除非另有说明,否则该功能是在 ECMAScript 6 中引入的(这是 Promise 被添加到该语言中的时间)。

词汇表

40.8.1 Promise.all()

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

40.8.2 Promise.race()

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

40.8.3 Promise.any() [ES2021]

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

这是 AggregateError 的类型签名(省略了一些成员)

class AggregateError {
  constructor(errors: Iterable<any>, message: string);
  get errors(): Array<any>;
  get message(): string;
}

40.8.4 Promise.allSettled() [ES2020]

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

这是 SettlementObject 的类型签名

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

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

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