原文链接:http://javascript.info/promise-api,translate with ❤️ by zhangbao.
在 Promise 上存在 4 个静态方法。本章将快速介绍它们的使用。
Promise.resolve
语法是:
let promise = Promise.resolve(value);
使用给定的参数 value
,返回 resolved 状态的 Promise。
等同于:
let promise = new Promise(resolve => resolve(value));
是在已知值的情况下使用这个方法,把它“包装”到 Promise 中。
例如,下面的 loadCached
函数 fetch 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。在 (*)
处 Primise.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 就 resolved 了。
例如,下面的 Primise.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 当所有的 Promise 都解决了——每个 Promise 都贡献了一个数组元素。
请注意,结果顺序和请求顺序是一致的。尽管第一个 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'
];
// 将每一个 url 映射到 promise fetch(github url)
let requests = urls.map(url => fetch(url));
// Promise.all 会一直等到所有的 Promise 都 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 => {
// 所有的请求都成功返回了,我们可以显示 HTTP 的请求状态码
for(let response of responses) {
alert(`${response.url}: ${response.status}`); // 每个 url 请求都显示 200
}
return responses;
})
// 将每个 response.json() 的结果(即接口返回结果的数据内容)映射到数组中
.then(responses => Promise.all(responses.map(r => r.json())))
// 返回数据解析为对象,“users”就是由这些对象组成的数组
.then(users => users.forEach(user => alert(user.name)));
如果任何一个 Promise reject 了,Promise.all
会携带错误信息立即 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); // Error: Whoops!
本例中,第二个 Promise 在两秒钟后 reject。这将导致 Promise.all
立即 reject,因此会执行 .catch
:reject 的错误信息成为整个 Promise.all
的输出结果。
Promise 最终的一个特点是,我们不能“取消”或者“中断”它的执行。因此其他的 Promise 会继续执行,最终解决,但他们的结果将被忽略。
有一些方法可以避免这种情况:我们可以在错误的情况下编写额外的代码来 clearTimeout
(或者其他取消方式)Promise,或者我们可以让错误出现在结果数组中(参见这一章稍后面的任务)。
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, // 看作 Promise.resolve(2)
3 // 看作 Promise.resolve(3)
]).then(alert); // 1, 2, 3
因此,我们能够方便地将非 Promise 值传递给
Promise.all
。
Promise.race
类似于 Proimse.all
,这个方法也接收由 Promise 组成的可迭代对象,但是不会等到所有的 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.race 的结果输出。在第一个 Promise 解决“赢得竞赛”后,其他的的结果/错误都会被忽略。
总结
在 Promise 上存在 4 个静态方法:
Promise.resolve(value)
:用给定的值创建一个 resolved 状态的 Promise 对象,Promise.reject(error)
:用给定的错误对象创建一个 rejected 状态的 Promise 对象,Promise.all(promises)
:等待所有的 Promise resolve 并且返回一个数组结果,如果其中任何一个 Promise reject 了,就成为Promise.all
的错误结果返回,所有其他结果都会被忽略。Promise.race(promises)
:等待第一个 Promise 解决,它的结果/错误成为Promise.race
的输出。
在这 4 个方法里,Promise.all
在实践中最常使用。
练习题
问题
一、容错版的 Promise.all
我们平行得 fetch 多个 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)))
// 展示每一个响应的状态码
.then(responses => { // (*)
for(let response of responses) {
alert(`${response.url}: ${response.status}`);
}
});
问题是,只要有一个请求失败了,Promise.all
就 reject 了,我们就丢失了其他的其他的请求结果。
这不太好。
修改代码,让 (*)
处的 responses
数组包含成功 fetch 的请求结果,错误对象中则包含失败的 fetch 请求。
例如,如果其中一个请求失败了,就应该这样:
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'http://no-such-url'
];
Promise.all(...) // fetch URL 地址的代码...
// 将 fetch 错误的请求结果(即错误对象)也放在结果数组里...
.then(responses => {
// 3 urls => 3 个数组成员
alert(responses[0].status); // 200
alert(responses[1].status); // 200
alert(responses[2]); // TypeError: failed to fetch (文本可能略有不同)
});
P.S. 本任务中,无需使用 response.text()
或 response.json()
加载完整的响应。只要能正确处理 fetch 失败的错误就行。
二、容错版的 Promise.all(需把返回数据处理成 JSON 对象)
将上一个任务改进一下。现在不是只要调用 fetch 就够了,也需要将从指定 URL 地址加载的数据解析成 JSON 对象。
这是代码:
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://api.github.com/users/jeresig'
];
// 产生 fetch 请求
Promise.all(urls.map(url => fetch(url)))
// 将每个 response 映射成 response.json()
.then(responses => Promise.all(
responses.map(r => r.json())
))
// 显示每个用户的名字
.then(users => { // (*)
for(let user of users) {
alert(user.name);
}
});
现在问题是,如果有请求失败了,Promise.all
就 reject 了,我们会丢失其他请求的结果。因此上面的代码不是能容错的,就像之前的任务那样。
修改代码,让 (*)
处的数组包含请求成功的和请求失败的结果。
需要注意的是,错误在 fetch()
(即网络请求失败)和 response.json()
(即响应不是有效的 JSON 数据) 时都有可能发生。这两种情况下的错误都会成为返回的结果数组里的成员。
答案
一、容错版的 Promise.all
解决方案还是挺简单的。
我们看下代码:
Promise.all([
fetch('https://api.github.com/users/iliakan'),
fetch('https://api.github.com/users/remy'),
fetch('http://no-such-url')
])
在 Promise.all
中,这里有一个由 fetch(...)
请求返回的 Promise 组成的数组。
我们不能修改 Promise.all
这个方法的行为:如果检测到错误,就会 reject。因此我们需要阻止错误发生。相反,当一个 fetch
发生错误的时候,我们需要把错误当做“正常”的结果。
就是下面这样做的:
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 中发生的错误,并且正常返回。根据 Promise 的运行规则,如果在 .then/catch
处理器中返回了一个值(不管是一个错误对象还是别的什么),就继续执行“正常”的流程。
因此,在 Promise.all
外部 .catch
返回的结果被当做“正常”结果返回了。
这段代码:
Promise.all(
urls.map(url => fetch(url))
)
可以被重写为:
Promise.all(
urls.map(url => fetch(url).catch(err => err))
)
二、容错版的 Promise.all(需把返回数据处理成 JSON 对象)
// 之前的 Promise 链会因为一个错误的发生,而整个 reject
// 修改为:
// 将出现的错误对象也作为结果数组的成员返回给用户
let urls = [
'https://api.github.com/users/iliakan',
// 请求这个 URL 地址得到是一个 HTML 网页,它是无效 JSON,因此 response.json() 会失败
'/',
// 这个 URL 地址是无效的,因此 fetch 失败
'http://no-such-url'
];
// 修改为:
Promise.all(urls.map(url => fetch(url).catch(err => err)))
.then(responses => Promise.all(
// 如果请求出错,直接返回错误对象
// 否则对 response.json() 做捕获错误的处理,返回它的处理结果
responses.map(r => r instanceof Error ? r : r.json().catch(err => err))
))
.then(results => {
alert(results[0].name); // Ilya Kantor
alert(results[1]); // SyntaxError: Unexpected token < in JSON at position 0
alert(results[2]); // TypeError: failed to fetch (text may vary)
});
在线查看。
(完)