Promise
類別中有 6 個靜態方法。我們將在此快速介紹它們的用例。
Promise.all
假設我們希望同時執行多個 promises,並等到它們都準備好。
例如,同時下載多個 URL,並在它們都完成後處理內容。
這就是 Promise.all
的用途。
語法為
let promise = Promise.all(iterable);
Promise.all
採用一個可迭代物件(通常是 promises 陣列),並傳回一個新的 promise。
當所有列出的 Promise 都已解決時,新的 Promise 會解決,且其結果陣列會成為其結果。
例如,以下的 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 的順序相同。即使第一個 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 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 被拒絕,則 Promise.all
回傳的 Promise 會立即拒絕並顯示該錯誤。
例如
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 會在兩秒後拒絕。這會導致 Promise.all
立即拒絕,因此執行 .catch
:拒絕錯誤會變成整個 Promise.all
的結果。
如果一個 Promise 被拒絕,Promise.all
會立即拒絕,完全忘記清單中的其他 Promise。它們的結果會被忽略。
例如,如果有多個 fetch
呼叫,就像上面的範例一樣,而且其中一個呼叫失敗,其他呼叫仍然會繼續執行,但 Promise.all
就不會再關注它們了。它們可能會解決,但它們的結果會被忽略。
Promise.all
沒有取消它們,因為 Promise 中沒有「取消」的概念。在 另一章 中,我們將介紹可以協助處理此問題的 AbortController
,但它不屬於 Promise API 的一部分。
Promise.all(iterable)
允許 iterable
中有非 Promise 的「一般」值通常,Promise.all(...)
會接受 Promise 的 iterable(在大部分情況下是陣列)。但如果這些物件中任何一個不是 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
如果任何 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
只會等待所有承諾完成,而不管結果為何。產生的陣列有
{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...}
]
因此對於每個承諾,我們會取得其狀態和 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)
,然後為每個承諾新增 .then
處理常式。
該處理常式會將成功的結果 value
轉換成 {status:'fulfilled', value}
,並將錯誤 reason
轉換成 {status:'rejected', reason}
。這正是 Promise.allSettled
的格式。
現在我們可以使用 Promise.allSettled
來取得所有給定承諾的結果,即使其中一些承諾會被拒絕。
Promise.race
類似於 Promise.all
,但只會等待第一個完成的承諾,並取得其結果(或錯誤)。
語法為
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.any
類似於 Promise.race
,但只會等待第一個完成的承諾,並取得其結果。如果所有給定的承諾都被拒絕,則會使用 AggregateError
拒絕回傳的承諾,這是一個特殊的錯誤物件,會將所有承諾錯誤儲存在其 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.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!
});
正如你所見,失敗承諾的錯誤物件可以在 AggregateError
物件的 errors
屬性中取得。
Promise.resolve/reject
在現代程式碼中很少需要 Promise.resolve
和 Promise.reject
方法,因為 async/await
語法(我們稍後會 介紹)讓它們有點過時了。
我們在這裡介紹它們是為了完整性,以及為了那些由於某些原因無法使用 async/await
的人。
Promise.resolve
Promise.resolve(value)
會建立一個已完成的承諾,其結果為 value
。
等同於
let promise = new Promise(resolve => resolve(value));
當預期函式會回傳承諾時,會使用這個方法來確保相容性。
例如,以下的 loadCached
函式會擷取一個 URL 並記住(快取)其內容。對於使用相同 URL 的後續呼叫,它會立即從快取中取得先前的內容,但會使用 Promise.resolve
來建立一個承諾,因此回傳值始終是一個承諾
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(…)
,因為保證函式會回傳一個承諾。我們可以在 loadCached
之後隨時使用 .then
。這就是 (*)
行中 Promise.resolve
的目的。
Promise.reject
Promise.reject(error)
會建立一個被拒絕的承諾,其錯誤為 error
。
等同於
let promise = new Promise((resolve, reject) => reject(error));
實際上,這種方法幾乎從未使用過。
摘要
Promise
類別有 6 個靜態方法
Promise.all(promises)
– 等待所有承諾解析,並傳回它們結果的陣列。如果任何給定的承諾遭到拒絕,它就會變成Promise.all
的錯誤,而所有其他結果都會被忽略。Promise.allSettled(promises)
(最近新增的方法) – 等待所有承諾解決,並傳回它們的結果,作為物件陣列,包含狀態
:"已完成"
或"已拒絕"
值
(如果已完成) 或原因
(如果已拒絕)。
Promise.race(promises)
– 等待第一個承諾解決,而它的結果/錯誤會變成結果。Promise.any(promises)
(最近新增的方法) – 等待第一個承諾完成,而它的結果會變成結果。如果所有給定的承諾都遭到拒絕,AggregateError
會變成Promise.any
的錯誤。Promise.resolve(value)
– 使用給定的值建立已解析的承諾。Promise.reject(error)
– 使用給定的錯誤建立已拒絕的承諾。
在所有這些當中,Promise.all
可能在實務上最常見。
留言
<code>
標籤,對於多行程式碼 – 請將它們包覆在<pre>
標籤中,對於超過 10 行的程式碼 – 請使用沙箱 (plnkr、jsbin、codepen…)