2022 年 2 月 20 日

Promise API

Promise 类中有 6 个静态方法。我们在这里快速介绍一下它们的用例。

Promise.all

假设我们希望并行执行多个 Promise,并等到所有 Promise 都准备就绪。

例如,并行下载多个 URL,并在所有下载完成后处理内容。

这就是 Promise.all 的作用。

语法为

let promise = Promise.all(iterable);

Promise.all 接受一个可迭代对象(通常是承诺数组),并返回一个新的承诺。

当列出的所有承诺都得到解决时,新的承诺就会得到解决,并且它们的结果数组将成为其结果。

例如,下面的 Promise.all 在 3 秒后解决,然后其结果是一个数组 [1, 2, 3]

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(alert); // 1,2,3 when promises are ready: each promise contributes an array member

请注意,结果数组成员的顺序与其源承诺中的顺序相同。即使第一个承诺花费最长时间才能解决,它仍然是结果数组中的第一个。

一个常见的技巧是将作业数据数组映射到承诺数组,然后将其包装到 Promise.all 中。

例如,如果我们有一个 URL 数组,我们可以像这样获取它们

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://api.github.com/users/jeresig'
];

// map every url to the promise of the fetch
let requests = urls.map(url => fetch(url));

// Promise.all waits until all jobs are resolved
Promise.all(requests)
  .then(responses => responses.forEach(
    response => alert(`${response.url}: ${response.status}`)
  ));

一个更大的示例,通过名称获取 GitHub 用户数组的用户信息(我们可以通过其 ID 获取商品数组,逻辑是相同的)

let names = ['iliakan', 'remy', 'jeresig'];

let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));

Promise.all(requests)
  .then(responses => {
    // all responses are resolved successfully
    for(let response of responses) {
      alert(`${response.url}: ${response.status}`); // shows 200 for every url
    }

    return responses;
  })
  // map array of responses into an array of response.json() to read their content
  .then(responses => Promise.all(responses.map(r => r.json())))
  // all JSON answers are parsed: "users" is the array of them
  .then(users => users.forEach(user => alert(user.name)));

如果任何承诺被拒绝,则 Promise.all 返回的承诺会立即拒绝该错误。

例如

Promise.all([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: Whoops!

此处第二个承诺在两秒内拒绝。这会导致 Promise.all 立即拒绝,因此执行 .catch:拒绝错误成为整个 Promise.all 的结果。

如果发生错误,则忽略其他承诺

如果一个承诺被拒绝,则 Promise.all 立即拒绝,完全忘记列表中的其他承诺。它们的结果被忽略。

例如,如果有多个 fetch 调用,如上面的示例所示,并且一个调用失败,则其他调用仍将继续执行,但 Promise.all 将不再监视它们。它们可能会解决,但它们的结果将被忽略。

Promise.all 不会做任何事情来取消它们,因为在承诺中没有“取消”的概念。在 另一章 中,我们将介绍 AbortController,它可以帮助解决此问题,但它不是 Promise API 的一部分。

Promise.all(iterable) 允许在 iterable 中使用非承诺的“常规”值

通常,Promise.all(...) 接受一个可迭代对象(大多数情况下为数组)的 Promise。但是,如果其中任何对象不是 Promise,它将“按原样”传递给结果数组。

例如,这里的结果是 [1, 2, 3]

Promise.all([
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000)
  }),
  2,
  3
]).then(alert); // 1, 2, 3

因此,我们可以将就绪值传递给 Promise.all,以便于使用。

Promise.allSettled

最近添加的功能
这是最近添加到语言中的功能。旧浏览器可能需要 polyfill

如果任何 Promise 被拒绝,Promise.all 将整体拒绝。这适用于“全有或全无”的情况,即我们需要所有结果都成功才能继续进行

Promise.all([
  fetch('/template.html'),
  fetch('/style.css'),
  fetch('/data.json')
]).then(render); // render method needs results of all fetches

Promise.allSettled 只是等待所有 Promise 完成,无论结果如何。结果数组具有

  • {status:"fulfilled", value:result} 表示成功的响应,
  • {status:"rejected", reason:error} 表示错误。

例如,我们希望获取多个用户的信息。即使一个请求失败,我们仍然对其他请求感兴趣。

让我们使用 Promise.allSettled

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://no-such-url'
];

Promise.allSettled(urls.map(url => fetch(url)))
  .then(results => { // (*)
    results.forEach((result, num) => {
      if (result.status == "fulfilled") {
        alert(`${urls[num]}: ${result.value.status}`);
      }
      if (result.status == "rejected") {
        alert(`${urls[num]}: ${result.reason}`);
      }
    });
  });

上面 (*) 行中的 results 将是

[
  {status: 'fulfilled', value: ...response...},
  {status: 'fulfilled', value: ...response...},
  {status: 'rejected', reason: ...error object...}
]

因此,对于每个 Promise,我们都会获取其状态和 value/error

Polyfill

如果浏览器不支持 Promise.allSettled,则很容易进行 polyfill

if (!Promise.allSettled) {
  const rejectHandler = reason => ({ status: 'rejected', reason });

  const resolveHandler = value => ({ status: 'fulfilled', value });

  Promise.allSettled = function (promises) {
    const convertedPromises = promises.map(p => Promise.resolve(p).then(resolveHandler, rejectHandler));
    return Promise.all(convertedPromises);
  };
}

在此代码中,promises.map 采用输入值,使用 p => Promise.resolve(p) 将它们转换为 Promise(以防传递了非 Promise),然后向每个值添加 .then 处理程序。

该处理程序将成功的结果 value 转换为 {status:'fulfilled', value},并将错误 reason 转换为 {status:'rejected', reason}。这正是 Promise.allSettled 的格式。

现在,我们可以使用 Promise.allSettled 来获取所有给定 Promise 的结果,即使其中一些 Promise 被拒绝。

Promise.race

类似于 Promise.all,但只等待第一个已完成的 Promise 并获取其结果(或错误)。

语法为

let promise = Promise.race(iterable);

例如,这里的结果将是 1

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1

这里第一个 Promise 最快,因此它成为结果。在第一个已完成的 Promise “赢得比赛”后,所有进一步的结果/错误都将被忽略。

Promise.any

类似于 Promise.race,但仅等待第一个已完成的 Promise 并获取其结果。如果给定的所有 Promise 都被拒绝,则返回的 Promise 将被拒绝,并带有 AggregateError,这是一个特殊的错误对象,它将所有 Promise 错误存储在它的 errors 属性中。

语法为

let promise = Promise.any(iterable);

例如,这里的结果将是 1

Promise.any([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 1000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1

这里的第一个 Promise 最快,但它被拒绝了,所以第二个 Promise 成为结果。在第一个已完成的 Promise “赢得比赛”后,所有进一步的结果都将被忽略。

下面是一个所有 Promise 都失败的示例

Promise.any([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ouch!")), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Error!")), 2000))
]).catch(error => {
  console.log(error.constructor.name); // AggregateError
  console.log(error.errors[0]); // Error: Ouch!
  console.log(error.errors[1]); // Error: Error!
});

如你所见,失败 Promise 的错误对象在 AggregateError 对象的 errors 属性中可用。

Promise.resolve/reject

在现代代码中很少需要 Promise.resolvePromise.reject 方法,因为 async/await 语法(我们将在 稍后介绍)使它们在某种程度上过时了。

我们在这里介绍它们是为了完整性,以及出于某种原因无法使用 async/await 的人。

Promise.resolve

Promise.resolve(value) 创建一个已解决的 Promise,其结果为 value

与以下相同

let promise = new Promise(resolve => resolve(value));

当期望函数返回 Promise 时,该方法用于兼容性。

例如,下面的 loadCached 函数获取一个 URL 并记住(缓存)其内容。对于使用相同 URL 的未来调用,它会立即从缓存中获取以前的内容,但使用 Promise.resolve 来生成它的 Promise,因此返回的值始终是一个 Promise

let cache = new Map();

function loadCached(url) {
  if (cache.has(url)) {
    return Promise.resolve(cache.get(url)); // (*)
  }

  return fetch(url)
    .then(response => response.text())
    .then(text => {
      cache.set(url,text);
      return text;
    });
}

我们可以编写 loadCached(url).then(…),因为该函数保证返回一个 Promise。我们始终可以在 loadCached 后使用 .then。这就是 Promise.resolve 在行 (*) 中的用途。

Promise.reject

Promise.reject(error) 创建一个被 error 拒绝的 Promise。

与以下相同

let promise = new Promise((resolve, reject) => reject(error));

在实践中,几乎从不使用此方法。

摘要

Promise 类有 6 个静态方法

  1. Promise.all(promises) – 等待所有 Promise 解析并返回其结果的数组。如果给定的任何 Promise 被拒绝,它将成为 Promise.all 的错误,所有其他结果都将被忽略。
  2. Promise.allSettled(promises)(最近添加的方法)– 等待所有 Promise 稳定并返回其结果,作为带有以下内容的对象数组
    • 状态“已完成”“已拒绝”
    • (如果已完成)或原因(如果已拒绝)。
  3. Promise.race(promises) – 等待第一个承诺解决,其结果/错误将成为结果。
  4. Promise.any(promises)(最近添加的方法) – 等待第一个承诺实现,其结果将成为结果。如果所有给定的承诺都被拒绝,则AggregateError将成为Promise.any的错误。
  5. Promise.resolve(value) – 使用给定值创建一个已解决的承诺。
  6. Promise.reject(error) – 使用给定的错误创建一个已拒绝的承诺。

在所有这些中,Promise.all可能是实践中最常见的。

教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议 - 请提交 GitHub 问题或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容 – 请详细说明。
  • 要插入几行代码,请使用<code>标记,对于多行 – 将它们包装在<pre>标记中,对于 10 行以上 – 使用沙箱(plnkrjsbincodepen…)