Promise 类中,有 4 中静态方法。我们在这里做下简单介绍。

Promise.resolve

语法:

let promise = Promise.resolve(value);

根据给定的 value 值返回 resolved promise。

等价于:

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

当我们已经有一个 value 的时候,就会使用该方法,但希望将它“封装”进 promise。

例如,下面的 loadCached 函数会获取 url 并记住结果,以便以后对同一 URL 进行调用时可以立即返回:

function loadCached(url) {
  let cache = loadCached.cache || (loadCached.cache = new Map());

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

  return fetch(url)
    .then(response => response.text())
    .then(text => {
      cache[url] = text;
      return text;
    });
}

我们可以使用 loadCached(url).then(…),因为函数保证会返回一个 promise。这是 Promise.resolve(*) 行的目的:它确保了接口的统一性。我们可以在 loadCached 之后使用 .then

Promise.reject

语法:

let promise = Promise.reject(error);

创建一个带有 error 的 rejected promise。

就像这样:

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

我们会在此讨论它的完整性,但在实际工作中,我们很少这样使用。

Promise.all

该方法并行运行多个 promise,并等待所有 promise 准备就绪。

语法:

let promise = Promise.all(iterable);

它需要一个带有 promise 的 iterable 对象,技术上来说,它是可以迭代的,但通常情况下,它只是一个数组,而且会返回一个新的 promise。新的 promise 是在所有 promise 都被解决并拥有一个存放结果的数组之后才出现的。

例如,下面的 Promise.all 在 3 秒之后被处理,然后它的结果就是一个 [1, 2, 3] 数组:

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

注意,它们的相对顺序是相同的。尽管第一个 promise 需要很长的时间来解决,但它仍然是结果数组中的第一个。

常见技巧是将一组作业数据映射到一个 promise 数组,然后再将它们封装进 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 fetch(github url)
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 ready, we can show HTTP status codes
    for(let response of responses) {
      alert(`${response.url}: ${response.status}`); // shows 200 for every url
    }

    return responses;
  })
  // map array of responses into 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 为 rejected,Promise.all 就会立即以 error reject。

例如:

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); // 错误:喔!

这里的第二个 promise 在两秒内为 reject。这立即导致了对 Promise.all 的 reject。因此 .catch 被执行:避免 error 成为整个 Promise.all 的结果。

重要的细节是 promise 没有提供 “cancel” 或 “abort” 执行方法。因此,其他 promise 会继续执行,并最终为 settle,但它们的结果会被忽略。

有避免这种情况的方法:我们可以编写额外的代码到 clearTimeout(或在出现 error 时取消)promise,或者我们可以将 error 作为结果数组中的成员显示出来(参阅本章下的 task)。

Promise.all(iterable) 允许在 iterable 中无 promise

通常 Promise.all(iterable) 接受可迭代的 promise(大多数情况是数组)。但如果这些对象中的任何一个不是 promise,它就会被封装进 Promise.resolve

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

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

因此我们可以在方便的时候将非 promise 值传递给 Promise.all

Promise.race

Promise.all 类似,所有的 promise 都是可迭代的,但不会等待所有都完成 —— 只等待第一个完成(或者有 error),然后继续执行。

语法是:

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.race 的结果。在第一个 promise “wins the race” 被解决后,所有的深层的结果/错误都会被忽略。

总结

Promise 类有 4 中静态方法:

  1. Promise.resolve(value) —— 根据给定值返回 resolved promise,
  2. Promise.reject(error) —— 根据给定错误返回 rejected promise,
  3. Promise.all(promises) —— 等待所有的 promise 为 resolve 时返回存放它们结果的数组。如果任意给定的 promise 为 reject,那么它就会变成 Promise.all 的错误结果,所以所有的其他结果都会被忽略。
  4. Promise.race(promises) —— 等待第一个 promise 被解决,其结果/错误即为结果。

这四个方法中,Promise.all 在实战中使用的最多。

任务

我们想要并行获取多个 URL。

执行此操作代码如下:

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

Promise.all(urls.map(url => fetch(url)))
  // for each response show its status
  .then(responses => { // (*)
    for(let response of responses) {
      alert(`${response.url}: ${response.status}`);
    }
  ));

问题是如果任何请求都失败了,那么 Promise.all 就会 reject error,而且所有的其他请求结果都会丢失。

这并不好。

修改代码会导致 (*) 行的 responses 数组包含成功响应的对象和失败时获取的 error 对象。

例如,如果其中一个 URL 失效,那么就会变成这样:

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

Promise.all(...) // your code to fetch URLs...
  // ...and pass fetch errors as members of the resulting array...
  .then(responses => {
    // 3 urls => 3 array members
    alert(responses[0].status); // 200
    alert(responses[1].status); // 200
    alert(responses[2]); // TypeError: failed to fetch (text may vary)
  });

P.S. 在这个任务中,你无需使用 response.text()response.json() 来加载完整的请求。只要正确处理 fetch 的 error 即可。

打开一个任务沙箱。

实际上解决方案非常简单。

就像这样:

Promise.all(
  fetch('https://api.github.com/users/iliakan'),
  fetch('https://api.github.com/users/remy'),
  fetch('http://no-such-url')
)

这里我们有一个指向 Promise.allfetch(...) promise 数组。

我们不能改变 Promise.all 的工作方式:如果它检测到 error,就会 reject 它。因此我们需要避免任何 error 发生。相反,如果 fetch 发生 error,我们需要将其视为“正常”结果。

就像这样:

Promise.all(
  fetch('https://api.github.com/users/iliakan').catch(err => err),
  fetch('https://api.github.com/users/remy').catch(err => err),
  fetch('http://no-such-url').catch(err => err)
)

换句话说,.catch 会对所有的 promise 产生 error,然后正常返回。根据 promise 的工作原理,只要 .then/catch 处理器返回值(无论是 error 对象或其他内容),执行流程就会“正常”进行。

因此 .catch 会将 error 作为“正常”结果返回给外部的 Promise.all

代码如下:

Promise.all(
  urls.map(url => fetch(url))
)

可重写为:

Promise.all(
  urls.map(url => fetch(url).catch(err => err))
)

使用沙箱打开解决方案。

改进之前 容错机制 Promise.all 任务的解决方案。我们现在只需要调用 fetch,但要从给定的 URL 中加载 JSON。

这是改进后的代码:

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

// make fetch requests
Promise.all(urls.map(url => fetch(url)))
  // map each response to response.json()
  .then(responses => Promise.all(
    responses.map(r => r.json())
  ))
  // show name of each user
  .then(users => {  // (*)
    for(let user of users) {
      alert(user.name);
    }
  });

问题是如果任意请求都失败了,那么 Promise.all 就会 reject error,而且会丢失其他所有请求的结果。因此上述代码不易于容错,与上一个任务相同。

请修改代码后,保证 (*) 的数组中包含请求成功解析后的 JSON 和错误的 JSON。

请注意,错误可能同时发生在 fetch(如果请求失败)和 response.json()(如果响应的是无效 JSON)中。在这两种情况下,错误都会成为结果对象的成员。

这两种情况沙箱都有。

打开一个任务沙箱。

教程路线图

评论

在评论之前先阅读本内容…
  • 欢迎你在文章下添加补充内容、提出你的问题或回答提出的问题。
  • 使用 <code> 标签插入几行代码,对于多行代码 — 可以使用 <pre>,对于超过十行的代码 — 建议使用沙箱(plnkrJSBincodepen 等)。
  • 如果你无法理解文章中的内容 — 请详细说明。