原文链接:http://javascript.info/promise-api,translate with ❤️ by zhangbao.

在 Promise 上存在 4 个静态方法。本章将快速介绍它们的使用。

Promise.resolve

语法是:

  1. let promise = Promise.resolve(value);

使用给定的参数 value,返回 resolved 状态的 Promise。

等同于:

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

是在已知值的情况下使用这个方法,把它“包装”到 Promise 中。

例如,下面的 loadCached 函数 fetch url 并且记住结果,这样在未来某个时刻再调用同一个 URL 时会立即返回结果。

  1. function loadCached(url) {
  2. let cache = loadCached.cache || (loadCached.cache = new Map());
  3. if (cache.has(url)) {
  4. return Promise.resolve(cache.get(url)); // (*)
  5. }
  6. return fetch(url)
  7. .then(response => response.text())
  8. .then(text => {
  9. cache[url] = text;
  10. return text;
  11. });
  12. }

我们可以使用 loadCached(url).then(...),因为函数始终返回一个 Promise。在 (*)Primise.resolve() 的目的是:保证接口是统一的,我们可以在 loadCached 之后使用 .then

Promise.reject

语法是:

  1. let promise = Promise.reject(error);

使用给定的错误参数 error,创建一个 rejected 状态的 Promise 对象。

等同于:

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

我们是为了完整性才在这里介绍它,在实际代码里很少使用它。

Promise.all

这个方法用来平行地同时执行多个 Promise,一直等到所有的 Promise 都解决了。

语法是:

  1. let promise = Promise.all(iterable);

它接收一个由 Promise 对象组成的 iterable 对象(技术上讲——可以是任意的可迭代对象,但是通常是一个数组),方法返回一个新的 Promise 对象。当所有的 Promise 都解决并产生一系列结果时,新的 Promise 就 resolved 了。

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

  1. Promise.all([
  2. new Promise((resolve, reject) => setTimeout(() => resolve(1), 3000)), // 1
  3. new Promise((resolve, reject) => setTimeout(() => resolve(2), 2000)), // 2
  4. new Promise((resolve, reject) => setTimeout(() => resolve(3), 1000)) // 3
  5. ]).then(alert); // 1,2,3 当所有的 Promise 都解决了——每个 Promise 都贡献了一个数组元素。

请注意,结果顺序和请求顺序是一致的。尽管第一个 Promise 需要花费最长的时间来解决,但它仍然是返回结果中的第一个元素。

一个常见技巧是将一组任合并到由 Promise 对象组成的数组中,然后将其包装到 Promise.all 中。

例如,我们有一个由 URL 组成的数组,我们可以这样使用:

  1. let urls = [
  2. 'https://api.github.com/users/iliakan',
  3. 'https://api.github.com/users/remy',
  4. 'https://api.github.com/users/jeresig'
  5. ];
  6. // 将每一个 url 映射到 promise fetch(github url)
  7. let requests = urls.map(url => fetch(url));
  8. // Promise.all 会一直等到所有的 Promise 都 resolved 了
  9. Promise.all(requests)
  10. .then(responses => responses.forEach
  11. response => alert(`${response.url}: ${response.status}`)
  12. ));

一个更真实的例子,通过一组 Github 用户获取用户信息(或者可以通过一组商品的 ID 获取商品信息,逻辑是一样的):

  1. let names = ['iliakan', 'remy', 'jeresig'];
  2. let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));
  3. Promise.all(requests)
  4. .then(responses => {
  5. // 所有的请求都成功返回了,我们可以显示 HTTP 的请求状态码
  6. for(let response of responses) {
  7. alert(`${response.url}: ${response.status}`); // 每个 url 请求都显示 200
  8. }
  9. return responses;
  10. })
  11. // 将每个 response.json() 的结果(即接口返回结果的数据内容)映射到数组中
  12. .then(responses => Promise.all(responses.map(r => r.json())))
  13. // 返回数据解析为对象,“users”就是由这些对象组成的数组
  14. .then(users => users.forEach(user => alert(user.name)));

如果任何一个 Promise reject 了,Promise.all 会携带错误信息立即 reject。

例如:

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

本例中,第二个 Promise 在两秒钟后 reject。这将导致 Promise.all 立即 reject,因此会执行 .catch :reject 的错误信息成为整个 Promise.all 的输出结果。

Promise 最终的一个特点是,我们不能“取消”或者“中断”它的执行。因此其他的 Promise 会继续执行,最终解决,但他们的结果将被忽略。

有一些方法可以避免这种情况:我们可以在错误的情况下编写额外的代码来 clearTimeout(或者其他取消方式)Promise,或者我们可以让错误出现在结果数组中(参见这一章稍后面的任务)。

Promise API - 图1Promise.all(iterable) 允许 iterable 中出现非 Promise 成员。

正常情况下,Promise.all(iterable) 接收一个由 Promise 对象成员组成的可迭代对象(大多数情况下是数组)。但是如果成员中有不是 Promise 的,就会被包装在 Promise.resolve 中。

例如,本例返回的结果是 [1, 2, 3]

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

因此,我们能够方便地将非 Promise 值传递给 Promise.all

Promise.race

类似于 Proimse.all,这个方法也接收由 Promise 组成的可迭代对象,但是不会等到所有的 Promise 都完成——而是只等到第一个结果(或者错误)出来。

语法是:

  1. let promise = Promise.race(iterable);

例如,这里的结果会是 1

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

因此,第一个结果/错误成为 Promise.race 的结果输出。在第一个 Promise 解决“赢得竞赛”后,其他的的结果/错误都会被忽略。

总结

在 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.race 的输出。

在这 4 个方法里,Promise.all 在实践中最常使用。

练习题

问题

一、容错版的 Promise.all

我们平行得 fetch 多个 URL 地址。

这里是代码:

  1. let urls = [
  2. 'https://api.github.com/users/iliakan',
  3. 'https://api.github.com/users/remy',
  4. 'https://api.github.com/users/jeresig'
  5. ];
  6. Promise.all(urls.map(url => fetch(url)))
  7. // 展示每一个响应的状态码
  8. .then(responses => { // (*)
  9. for(let response of responses) {
  10. alert(`${response.url}: ${response.status}`);
  11. }
  12. });

问题是,只要有一个请求失败了,Promise.all 就 reject 了,我们就丢失了其他的其他的请求结果。

这不太好。

修改代码,让 (*) 处的 responses 数组包含成功 fetch 的请求结果,错误对象中则包含失败的 fetch 请求。

例如,如果其中一个请求失败了,就应该这样:

  1. let urls = [
  2. 'https://api.github.com/users/iliakan',
  3. 'https://api.github.com/users/remy',
  4. 'http://no-such-url'
  5. ];
  6. Promise.all(...) // fetch URL 地址的代码...
  7. // 将 fetch 错误的请求结果(即错误对象)也放在结果数组里...
  8. .then(responses => {
  9. // 3 urls => 3 个数组成员
  10. alert(responses[0].status); // 200
  11. alert(responses[1].status); // 200
  12. alert(responses[2]); // TypeError: failed to fetch (文本可能略有不同)
  13. });

P.S. 本任务中,无需使用 response.text()response.json() 加载完整的响应。只要能正确处理 fetch 失败的错误就行。

二、容错版的 Promise.all(需把返回数据处理成 JSON 对象)

将上一个任务改进一下。现在不是只要调用 fetch 就够了,也需要将从指定 URL 地址加载的数据解析成 JSON 对象。

这是代码:

  1. let urls = [
  2. 'https://api.github.com/users/iliakan',
  3. 'https://api.github.com/users/remy',
  4. 'https://api.github.com/users/jeresig'
  5. ];
  6. // 产生 fetch 请求
  7. Promise.all(urls.map(url => fetch(url)))
  8. // 将每个 response 映射成 response.json()
  9. .then(responses => Promise.all(
  10. responses.map(r => r.json())
  11. ))
  12. // 显示每个用户的名字
  13. .then(users => { // (*)
  14. for(let user of users) {
  15. alert(user.name);
  16. }
  17. });

现在问题是,如果有请求失败了,Promise.all 就 reject 了,我们会丢失其他请求的结果。因此上面的代码不是能容错的,就像之前的任务那样。

修改代码,让 (*) 处的数组包含请求成功的和请求失败的结果。

需要注意的是,错误在 fetch()(即网络请求失败)和 response.json()(即响应不是有效的 JSON 数据) 时都有可能发生。这两种情况下的错误都会成为返回的结果数组里的成员。

答案

一、容错版的 Promise.all

解决方案还是挺简单的。

我们看下代码:

  1. Promise.all([
  2. fetch('https://api.github.com/users/iliakan'),
  3. fetch('https://api.github.com/users/remy'),
  4. fetch('http://no-such-url')
  5. ])

Promise.all 中,这里有一个由 fetch(...) 请求返回的 Promise 组成的数组。

我们不能修改 Promise.all 这个方法的行为:如果检测到错误,就会 reject。因此我们需要阻止错误发生。相反,当一个 fetch 发生错误的时候,我们需要把错误当做“正常”的结果。

就是下面这样做的:

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

也就是说,.catch 捕获了所有 Promise 中发生的错误,并且正常返回。根据 Promise 的运行规则,如果在 .then/catch 处理器中返回了一个值(不管是一个错误对象还是别的什么),就继续执行“正常”的流程。

因此,在 Promise.all 外部 .catch 返回的结果被当做“正常”结果返回了。

这段代码:

  1. Promise.all(
  2. urls.map(url => fetch(url))
  3. )

可以被重写为:

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

二、容错版的 Promise.all(需把返回数据处理成 JSON 对象)

  1. // 之前的 Promise 链会因为一个错误的发生,而整个 reject
  2. // 修改为:
  3. // 将出现的错误对象也作为结果数组的成员返回给用户
  4. let urls = [
  5. 'https://api.github.com/users/iliakan',
  6. // 请求这个 URL 地址得到是一个 HTML 网页,它是无效 JSON,因此 response.json() 会失败
  7. '/',
  8. // 这个 URL 地址是无效的,因此 fetch 失败
  9. 'http://no-such-url'
  10. ];
  11. // 修改为:
  12. Promise.all(urls.map(url => fetch(url).catch(err => err)))
  13. .then(responses => Promise.all(
  14. // 如果请求出错,直接返回错误对象
  15. // 否则对 response.json() 做捕获错误的处理,返回它的处理结果
  16. responses.map(r => r instanceof Error ? r : r.json().catch(err => err))
  17. ))
  18. .then(results => {
  19. alert(results[0].name); // Ilya Kantor
  20. alert(results[1]); // SyntaxError: Unexpected token < in JSON at position 0
  21. alert(results[2]); // TypeError: failed to fetch (text may vary)
  22. });

在线查看

(完)