Promise.resolve()
:建立一個 Promise,並以給定的值作為已完成狀態Promise.reject()
:建立一個 Promise,並以給定的值作為拒絕狀態.then()
回呼函式中傳回和擲回.catch()
及其回呼函式.finally()
[ES2018]XMLHttpRequest
轉為 Promiseutil.promisify()
Promise.all()
Promise.race()
Promise.any()
和 AggregateError
[ES2021]Promise.allSettled()
[ES2020]Promise.all()
(進階)
Promise.all()
是 fork-joinPromise.all()
Promise.race()
Promise.any()
[ES2021]Promise.allSettled()
[ES2020] 建議閱讀
本章節建立在 前一章節 的基礎上,提供 JavaScript 中非同步程式設計的背景知識。
Promise 是一種非同步傳遞結果的技術。
以下程式碼是使用 Promise 為基礎的函式 addAsync()
的範例(其實作將在稍後顯示)
addAsync(3, 4)
.then(result => { // success
.equal(result, 7);
assert
}).catch(error => { // failure
.fail(error);
assert; })
Promise 類似於 事件模式:有一個物件(一個 Promise),我們可以在其中註冊回呼函式
.then()
註冊處理結果的回呼函式。.catch()
註冊處理錯誤的回呼函式。Promise 為基礎的函式會傳回一個 Promise,並在完成時傳送結果或錯誤給它。Promise 會將其傳遞給相關的回呼函式。
與事件模式不同,Promise 已針對一次性結果進行最佳化
.then()
和 .catch()
,因為它們都會傳回 Promise。這有助於依序呼叫多個非同步函式。稍後會詳細說明。什麼是 Promise?有兩種觀點
以下是 Promise 為基礎的函式,用於加總兩個數字 x
和 y
function addAsync(x, y) {
return new Promise(
, reject) => { // (A)
(resolveif (x === undefined || y === undefined) {
reject(new Error('Must provide two parameters'));
else {
} resolve(x + y);
};
}) }
addAsync()
會立即呼叫 Promise
建構函式。該函式的實際實作存在於傳遞給該建構函式的回呼函式中(A 行)。該回呼函式提供了兩個函式
resolve
用於傳遞結果(在成功的情況下)。reject
用於傳送錯誤(在失敗的情況下)。圖 22 描繪了承諾可以處於的三個狀態。承諾專門處理一次性結果,並保護我們免於競爭條件(過早或過晚註冊)
.then()
回呼或 .catch()
回呼,則一旦承諾已解決,就會收到通知。.then()
或 .catch()
,它們會收到快取值。此外,一旦承諾已解決,其狀態和解決值就無法再變更。這有助於使程式碼可預測,並強制執行承諾的一次性性質。
有些承諾永遠不會解決
承諾有可能永遠不會解決。例如
new Promise(() => {})
Promise.resolve()
:使用給定的值建立已完成的承諾Promise.resolve(x)
建立一個已完成的承諾,其值為 x
Promise.resolve(123)
.then(x => {
.equal(x, 123);
assert; })
如果參數已經是承諾,則會不變地傳回
const abcPromise = Promise.resolve('abc');
.equal(
assertPromise.resolve(abcPromise),
; abcPromise)
因此,給定任意值 x
,我們可以使用 Promise.resolve(x)
來確保我們有一個承諾。
請注意,名稱是 resolve
,而不是 fulfill
,因為如果其參數是已拒絕的承諾,.resolve()
會傳回已拒絕的承諾。
Promise.reject()
:使用給定的值建立已拒絕的承諾Promise.reject(err)
建立一個已拒絕的承諾,其值為 err
const myError = new Error('My error!');
Promise.reject(myError)
.catch(err => {
.equal(err, myError);
assert; })
.then()
回呼中傳回和擲回.then()
處理承諾完成。它也會傳回一個新的承諾。該承諾如何解決取決於回呼中發生了什麼事。我們來看三個常見的情況。
首先,回呼可以傳回非承諾值(A 行)。因此,.then()
傳回的承諾會以該值完成(如 B 行所檢查的)
Promise.resolve('abc')
.then(str => {
return str + str; // (A)
}).then(str2 => {
.equal(str2, 'abcabc'); // (B)
assert; })
其次,回呼可以傳回承諾 p
(A 行)。因此,p
「變成」.then()
傳回的內容。換句話說:.then()
已經傳回的承諾實際上被 p
取代了。
Promise.resolve('abc')
.then(str => {
return Promise.resolve(123); // (A)
}).then(num => {
.equal(num, 123);
assert; })
為什麼這很有用?我們可以傳回基於承諾的運算結果,並透過「扁平」(非巢狀).then()
處理其完成值。比較
// Flat
asyncFunc1()
.then(result1 => {
/*···*/
return asyncFunc2();
}).then(result2 => {
/*···*/
;
})
// Nested
asyncFunc1()
.then(result1 => {
/*···*/
asyncFunc2()
.then(result2 => {
/*···*/
;
}); })
第三,回呼可以擲回例外。因此,.then()
傳回的承諾會以該例外拒絕。也就是說,同步錯誤會轉換成非同步錯誤。
const myError = new Error('My error!');
Promise.resolve('abc')
.then(str => {
throw myError;
}).catch(err => {
.equal(err, myError);
assert; })
.catch()
及其回呼.then()
和 .catch()
的差別在於後者是由拒絕觸發,而不是完成。不過,這兩種方法都會將其回呼動作轉換成 Promise,方式相同。例如,在以下程式碼中,第 A 行 .catch()
回呼傳回的值會變成完成值
const err = new Error();
Promise.reject(err)
.catch(e => {
.equal(e, err);
assert// Something went wrong, use a default value
return 'default value'; // (A)
}).then(str => {
.equal(str, 'default value');
assert; })
.then()
和 .catch()
永遠會傳回 Promise。這讓我們可以建立任意長度的串接方法呼叫
function myAsyncFunc() {
return asyncFunc1() // (A)
.then(result1 => {
// ···
return asyncFunc2(); // a Promise
}).then(result2 => {
// ···
return result2 ?? '(Empty)'; // not a Promise
}).then(result3 => {
// ···
return asyncFunc4(); // a Promise
;
}) }
由於串接,第 A 行的 return
會傳回最後一個 .then()
的結果。
某種程度上,.then()
是同步分號的非同步版本
.then()
會依序執行兩個非同步作業。我們也可以將 .catch()
加入組合,讓它同時處理多個錯誤來源
asyncFunc1()
.then(result1 => {
// ···
return asyncFunction2();
}).then(result2 => {
// ···
}).catch(error => {
// Failure: handle errors of asyncFunc1(), asyncFunc2()
// and any (sync) exceptions thrown in previous callbacks
; })
.finally()
[ES2018]Promise 方法 .finally()
通常會像這樣使用
somePromise.then((result) => {
// ···
}).catch((error) => {
// ···
}).finally(() => {
// ···
});
.finally()
回呼會永遠執行,與 somePromise
和 .then()
和/或 .catch()
傳回的值無關。相反地
.then()
回呼只有在 somePromise
完成時才會執行。.catch()
回呼只有在
somePromise
被拒絕時,.then()
回呼傳回一個被拒絕的 Promise 時,.then()
回呼擲回一個例外時才會執行。.finally()
會忽略其回呼傳回的內容,並直接傳遞在呼叫之前存在的結算
Promise.resolve(123)
.finally(() => {})
.then((result) => {
.equal(result, 123);
assert;
})
Promise.reject('error')
.finally(() => {})
.catch((error) => {
.equal(error, 'error');
assert; })
不過,如果 .finally()
回呼擲回一個例外,.finally()
傳回的 Promise 會被拒絕
Promise.reject('error (originally)')
.finally(() => {
throw 'error (finally)';
}).catch((error) => {
.equal(error, 'error (finally)');
assert; })
.finally()
的使用案例:清理.finally()
的一個常見使用案例類似於同步 finally
子句的常見使用案例:在你使用完資源後進行清理。這應該永遠發生,無論一切是否順利或是有錯誤,例如
let connection;
.open()
db.then((conn) => {
= conn;
connection return connection.select({ name: 'Jane' });
}).then((result) => {
// Process result
// Use `connection` to make more queries
})// ···
.catch((error) => {
// handle errors
}).finally(() => {
.close();
connection; })
.finally()
的使用案例:在任何類型的結算後先做某件事我們也可以在 .then()
和 .catch()
之前使用 .finally()
。然後,我們在 .finally()
回呼中執行的動作會永遠在其他兩個回呼之前執行。
例如,這是完成的 Promise 會發生的情況
Promise.resolve('fulfilled')
.finally(() => {
console.log('finally');
}).then((result) => {
console.log('then ' + result);
}).catch((error) => {
console.log('catch ' + error);
});
// Output:
// 'finally'
// 'then fulfilled'
這是被拒絕的 Promise 會發生的情況
Promise.reject('rejected')
.finally(() => {
console.log('finally');
}).then((result) => {
console.log('then ' + result);
}).catch((error) => {
console.log('catch ' + error);
});
// Output:
// 'finally'
// 'catch rejected'
在處理一次性結果時,Promise 優於一般回呼的一些優點
基於 Promise 的函數和方法的類型簽章較為簡潔:如果函數是基於回呼,某些參數是關於輸入,而結尾的一個或兩個回呼是關於輸出。有了 Promise,所有與輸出相關的內容都會透過傳回的值處理。
鏈接非同步處理步驟更方便。
Promises 處理非同步錯誤(透過拒絕)和同步錯誤:在 new Promise()
、.then()
和 .catch()
的回呼函式中,例外狀況會轉換為拒絕。相反地,如果我們使用回呼函式進行非同步處理,例外狀況通常不會由我們處理;我們必須自己處理。
Promises 是逐漸取代許多不相容替代方案的單一標準。例如,在 Node.js 中,許多函式現在都提供基於 Promise 的版本。而新的非同步瀏覽器 API 通常都基於 Promise。
Promises 最大的優點之一是不需要直接使用它們:它們是非同步函式的基礎,一種用於執行非同步運算的同步語法。非同步函式在 下一章 中會說明。
透過實際使用 Promises,有助於了解它們。我們來看一些範例。
考慮以下包含 JSON 資料 的文字檔 person.json
{
"first": "Jane",
"last": "Doe"
}
我們來看兩個版本的程式碼,用來讀取此檔案並將其解析成物件。首先,一個基於回呼函式的版本。其次,一個基於 Promise 的版本。
以下程式碼會讀取此檔案的內容,並將其轉換成 JavaScript 物件。它是基於 Node.js 風格的回呼函式
import * as fs from 'fs';
.readFile('person.json',
fs, text) => {
(errorif (error) { // (A)
// Failure
.fail(error);
assertelse {
} // Success
try { // (B)
const obj = JSON.parse(text); // (C)
.deepEqual(obj, {
assertfirst: 'Jane',
last: 'Doe',
;
})catch (e) {
} // Invalid JSON
.fail(e);
assert
}
}; })
fs
是 Node.js 內建的檔案系統操作模組。我們使用基於回呼函式的函式 fs.readFile()
來讀取名稱為 person.json
的檔案。如果成功,內容會透過參數 text
以字串的形式傳遞。在 C 行,我們將該字串從基於文字的資料格式 JSON 轉換成 JavaScript 物件。JSON
是包含用於使用和產生 JSON 的方法的物件。它是 JavaScript 標準函式庫的一部分,並在 本書後續章節 中說明。
請注意,有兩個錯誤處理機制:A 行的 if
處理 fs.readFile()
回報的非同步錯誤,而 B 行的 try
則處理 JSON.parse()
回報的同步錯誤。
以下程式碼使用 readFileAsync()
,這是 fs.readFile()
的基於 Promise 的版本(透過 util.promisify()
建立,後續會說明)
readFileAsync('person.json')
.then(text => { // (A)
// Success
const obj = JSON.parse(text);
.deepEqual(obj, {
assertfirst: 'Jane',
last: 'Doe',
;
})
}).catch(err => { // (B)
// Failure: file I/O error or JSON syntax error
.fail(err);
assert; })
函式 readFileAsync()
會傳回一個 Promise。在 A 行,我們透過該 Promise 的方法 .then()
指定一個成功回呼。then
回呼中的其餘程式碼是同步的。
.then()
會傳回一個 Promise,這讓 B 行能夠呼叫 Promise 方法 .catch()
。我們使用它來指定一個失敗回呼。
請注意,.catch()
讓我們可以處理 readFileAsync()
的非同步錯誤和 JSON.parse()
的同步錯誤,因為 .then()
回呼中的例外會變成拒絕。
XMLHttpRequest
承諾化我們先前已經看過用於在網路瀏覽器中下載資料的事件為基礎的 XMLHttpRequest
API。下列函式讓該 API 承諾化
function httpGet(url) {
return new Promise(
, reject) => {
(resolveconst xhr = new XMLHttpRequest();
.onload = () => {
xhrif (xhr.status === 200) {
resolve(xhr.responseText); // (A)
else {
} // Something went wrong (404, etc.)
reject(new Error(xhr.statusText)); // (B)
}
}.onerror = () => {
xhrreject(new Error('Network error')); // (C)
;
}.open('GET', url);
xhr.send();
xhr;
}) }
請注意 XMLHttpRequest
的結果和錯誤是如何透過 resolve()
和 reject()
處理的
以下是使用 httpGet()
的方式
httpGet('http://example.com/textfile.txt')
.then(content => {
.equal(content, 'Content of textfile.txt\n');
assert
}).catch(error => {
.fail(error);
assert; })
練習:讓 Promise 超時
exercises/promises/promise_timeout_test.mjs
util.promisify()
util.promisify()
是一個實用函式,它會將一個基於回呼的函式 f
轉換成一個基於 Promise 的函式。也就是說,我們會從這個型別簽章
f(arg_1, ···, arg_n, (err: Error, result: T) => void) : void
轉換到這個型別簽章
f(arg_1, ···, arg_n) : Promise<T>
下列程式碼讓基於回呼的 fs.readFile()
承諾化 (A 行) 並使用它
import * as fs from 'fs';
import {promisify} from 'util';
const readFileAsync = promisify(fs.readFile); // (A)
readFileAsync('some-file.txt', {encoding: 'utf8'})
.then(text => {
.equal(text, 'The content of some-file.txt\n');
assert
}).catch(err => {
.fail(err);
assert; })
練習:
util.promisify()
util.promisify()
:exercises/promises/read_file_async_exrc.mjs
util.promisify()
:exercises/promises/my_promisify_test.mjs
所有現代瀏覽器都支援 Fetch,這是一個新的基於 Promise 的 API,用於下載資料。可以將它視為 XMLHttpRequest
的基於 Promise 的版本。以下是 該 API 的摘錄
interface Body {
text() : Promise<string>;
···
}interface Response extends Body {
···
}declare function fetch(str) : Promise<Response>;
這表示我們可以使用 fetch()
如下所示
fetch('http://example.com/textfile.txt')
.then(response => response.text())
.then(text => {
.equal(text, 'Content of textfile.txt\n');
assert; })
練習:使用 fetch API
exercises/promises/fetch_json_test.mjs
實作函式和方法的規則
不要混用(非同步)拒絕和(同步)例外。
這會讓我們的同步和非同步程式碼更具可預測性和更簡單,因為我們可以始終專注於單一錯誤處理機制。
對於基於 Promise 的函式和方法,此規則表示它們不應拋出例外。唉!很容易不小心出錯,例如
// Don’t do this
function asyncFunc() {
doSomethingSync(); // (A)
return doSomethingAsync()
.then(result => {
// ···
;
}) }
問題在於如果在 A 行拋出例外,則 asyncFunc()
將拋出例外。該函式的呼叫者只預期拒絕,而沒有為例外做好準備。我們可以透過三種方式來修正此問題。
我們可以將函式的整個主體包在 try-catch
陳述式中,如果拋出例外,則傳回已拒絕的 Promise
// Solution 1
function asyncFunc() {
try {
doSomethingSync();
return doSomethingAsync()
.then(result => {
// ···
;
})catch (err) {
} return Promise.reject(err);
} }
由於 .then()
會將例外轉換為拒絕,因此我們可以在 .then()
回呼中執行 doSomethingSync()
。為此,我們透過 Promise.resolve()
開始 Promise 鏈。我們忽略該初始 Promise 的完成值 undefined
。
// Solution 2
function asyncFunc() {
return Promise.resolve()
.then(() => {
doSomethingSync();
return doSomethingAsync();
}).then(result => {
// ···
;
}) }
最後,new Promise()
也會將例外轉換為拒絕。因此,使用此建構函式與前一個解決方案類似
// Solution 3
function asyncFunc() {
return new Promise((resolve, reject) => {
doSomethingSync();
resolve(doSomethingAsync());
}).then(result => {
// ···
;
}) }
大多數基於 Promise 的函式會以下列方式執行
下列程式碼示範了這一點
function asyncFunc() {
console.log('asyncFunc');
return new Promise(
, _reject) => {
(resolveconsole.log('new Promise()');
resolve();
;
})
}console.log('START');
asyncFunc()
.then(() => {
console.log('.then()'); // (A)
;
})console.log('END');
// Output:
// 'START'
// 'asyncFunc'
// 'new Promise()'
// 'END'
// '.then()'
我們可以看到 new Promise()
的回呼會在程式碼結束前執行,而結果會在稍後傳遞(A 行)。
此方法的好處
同步開始有助於避免競爭條件,因為我們可以依賴基於 Promise 的函式開始的順序。在 下一章 有個範例,其中文字會寫入檔案,並避免競爭條件。
串接 Promise 不會讓其他任務缺乏處理時間,因為在 Promise 解決之前,總會有一個中斷,事件迴圈可以在其中執行。
基於 Promise 的函式總是會非同步傳回結果;我們可以確定永遠不會有同步傳回。這種可預測性讓程式碼更容易處理。
有關此方法的更多資訊
「為非同步設計 API」,作者 Isaac Z. Schlueter
組合模式 是函式程式設計中用於建立結構的模式。它基於兩種函式
在 JavaScript Promises 的情況下
基本函式包括:Promise.resolve()
、Promise.reject()
組合函式包括:Promise.all()
、Promise.race()
、Promise.any()
、Promise.allSettled()
。在每個案例中
接下來,我們將仔細探討所提到的 Promise 組合器。
Promise.all()
這是 Promise.all()
的類型簽章
Promise.all<T>(promises: Iterable<Promise<T>>): Promise<Array<T>>
Promise.all()
傳回一個 Promise,其
promises
都已完成,則會完成。
promises
完成值的陣列。這是輸出 Promise 已完成的快速示範
const promises = [
Promise.resolve('result a'),
Promise.resolve('result b'),
Promise.resolve('result c'),
;
]Promise.all(promises)
.then((arr) => assert.deepEqual(
, ['result a', 'result b', 'result c']
arr; ))
以下範例說明如果至少一個輸入 Promise 被拒絕,會發生什麼事
const promises = [
Promise.resolve('result a'),
Promise.resolve('result b'),
Promise.reject('ERROR'),
;
]Promise.all(promises)
.catch((err) => assert.equal(
, 'ERROR'
err; ))
圖 23 說明 Promise.all()
的運作方式。
Promise.all()
進行非同步 .map()
陣列轉換方法(例如 .map()
、.filter()
等)是用於同步運算。例如
function timesTwoSync(x) {
return 2 * x;
}const arr = [1, 2, 3];
const result = arr.map(timesTwoSync);
.deepEqual(result, [2, 4, 6]); assert
如果 .map()
的回呼函式是基於 Promise 的函式(將一般值對應到 Promise 的函式),會發生什麼事?然後 .map()
的結果是 Promise 的陣列。唉,這不是一般程式碼可以處理的資料。還好,我們可以透過 Promise.all()
來修正:它會將 Promise 的陣列轉換成已完成一般值陣列的 Promise。
function timesTwoAsync(x) {
return new Promise(resolve => resolve(x * 2));
}const arr = [1, 2, 3];
const promiseArr = arr.map(timesTwoAsync);
Promise.all(promiseArr)
.then(result => {
.deepEqual(result, [2, 4, 6]);
assert; })
.map()
範例接下來,我們將使用 .map()
和 Promise.all()
從網路下載文字檔。為此,我們需要以下工具函式
function downloadText(url) {
return fetch(url)
.then((response) => { // (A)
if (!response.ok) { // (B)
throw new Error(response.statusText);
}return response.text(); // (C)
;
}) }
downloadText()
使用基於 Promise 的 fetch API 將文字檔下載為字串
response
(A 行)。response.ok
(B 行)會檢查是否有「檔案未找到」等錯誤。.text()
(C 行)將檔案內容擷取為字串。在以下範例中,我們下載兩個文字檔
const urls = [
'http://example.com/first.txt',
'http://example.com/second.txt',
;
]
const promises = urls.map(
=> downloadText(url));
url
Promise.all(promises)
.then(
=> assert.deepEqual(
(arr) , ['First!', 'Second!']
arr; ))
Promise.all()
的簡單實作這是 Promise.all()
的簡化實作(例如,它不會執行任何安全檢查)
function all(iterable) {
return new Promise((resolve, reject) => {
let elementCount = 0;
let result;
let index = 0;
for (const promise of iterable) {
// Preserve the current value of `index`
const currentIndex = index;
.then(
promise=> {
(value) = value;
result[currentIndex] ++;
elementCountif (elementCount === result.length) {
resolve(result); // (A)
},
}=> {
(err) reject(err); // (B)
;
})++;
index
}if (index === 0) {
resolve([]);
return;
}// Now we know how many Promises there are in `iterable`.
// We can wait until now with initializing `result` because
// the callbacks of .then() are executed asynchronously.
= new Array(index);
result ;
}) }
結果 Promise 會被解決的兩個主要位置是 A 行和 B 行。其中一個位置解決後,另一個位置就不能再變更解決值,因為 Promise 只會被解決一次。
Promise.race()
這是 Promise.race()
的類型簽章
Promise.race<T>(promises: Iterable<Promise<T>>): Promise<T>
Promise.race()
會傳回一個 Promise q
,只要 promises
中的第一個 Promise p
被解決,它就會被解決。q
具有與 p
相同的解決值。
在以下範例中,已完成 Promise 的解決(A 行)會在已拒絕 Promise 的解決(B 行)之前發生。因此,結果也會已完成(C 行)。
const promises = [
new Promise((resolve, reject) =>
setTimeout(() => resolve('result'), 100)), // (A)
new Promise((resolve, reject) =>
setTimeout(() => reject('ERROR'), 200)), // (B)
;
]Promise.race(promises)
.then((result) => assert.equal( // (C)
, 'result')); result
在以下範例中,拒絕會先發生
const promises = [
new Promise((resolve, reject) =>
setTimeout(() => resolve('result'), 200)),
new Promise((resolve, reject) =>
setTimeout(() => reject('ERROR'), 100)),
;
]Promise.race(promises)
.then(
=> assert.fail(),
(result) => assert.equal(
(err) , 'ERROR')); err
請注意,Promise.race()
傳回的 Promise 會在其輸入 Promise 中的第一個被解決後立即被解決。這表示 Promise.race([])
的結果永遠不會被解決。
圖 24 說明 Promise.race()
的運作方式。
Promise.race()
為 Promise 設定逾時在本節中,我們將使用 Promise.race()
為 Promise 設定逾時。以下輔助函式會在多次使用時派上用場
function resolveAfter(ms, value=undefined) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(value), ms);
;
}) }
resolveAfter()
會傳回一個 Promise,並在經過 ms
毫秒後以 value
解決。
此函式會為 Promise 設定逾時
function timeout(timeoutInMs, promise) {
return Promise.race([
,
promiseresolveAfter(timeoutInMs,
Promise.reject(new Error('Operation timed out'))),
;
]) }
timeout()
會傳回一個 Promise,其解決與以下兩個 Promise 中首先解決的 Promise 相同
promise
timeoutInMs
毫秒後會被拒絕的 Promisetimeout()
會使用一個事實來產生第二個 Promise,也就是以已拒絕 Promise 解決待處理 Promise 會導致前者被拒絕。
讓我們看看 timeout()
的實際運作。在此,輸入 Promise 會在逾時之前完成。因此,輸出 Promise 會完成。
timeout(200, resolveAfter(100, 'Result!'))
.then(result => assert.equal(result, 'Result!'));
在此,逾時會在輸入 Promise 完成之前發生。因此,輸出 Promise 會被拒絕。
timeout(100, resolveAfter(2000, 'Result!'))
.catch(err => assert.deepEqual(err, new Error('Operation timed out')));
了解「為 Promise 設定逾時」的真正意義非常重要
也就是說,設定逾時只會阻止輸入 Promise 影響輸出(因為 Promise 只會被解決一次)。但它不會停止產生輸入 Promise 的非同步操作。
Promise.race()
的簡單實作這是 Promise.race()
的簡化實作(例如,它不會執行安全性檢查)
function race(iterable) {
return new Promise((resolve, reject) => {
for (const promise of iterable) {
.then(
promise=> {
(value) resolve(value); // (A)
,
}=> {
(err) reject(err); // (B)
;
})
};
}) }
結果 Promise 會在 A 行或 B 行解決。一旦解決,解決值就無法再變更。
Promise.any()
和 AggregateError
[ES2021]這是 Promise.any()
的類型簽章
Promise.any<T>(promises: Iterable<Promise<T>>): Promise<T>
Promise.any()
會傳回 Promise p
。它的解決方式取決於參數 promises
(它指的是 Promise 的可迭代物件)
p
會以該 Promise 解析。p
會以包含所有拒絕值的 AggregateError
實例拒絕。這是 AggregateError
(Error
的子類別)的類型簽章
class AggregateError extends Error {
// Instance properties (complementing the ones of Error)
: Array<any>;
errors
constructor(
: Iterable<any>,
errors: string = '',
message?: ErrorOptions // ES2022
options;
)
}interface ErrorOptions {
?: any; // ES2022
cause }
圖 25 說明了 Promise.any()
的運作方式。
如果一個 Promise 已完成,就會發生以下情況
const promises = [
Promise.reject('ERROR A'),
Promise.reject('ERROR B'),
Promise.resolve('result'),
;
]Promise.any(promises)
.then((result) => assert.equal(
, 'result'
result; ))
如果所有 Promise 都被拒絕,就會發生以下情況
const promises = [
Promise.reject('ERROR A'),
Promise.reject('ERROR B'),
Promise.reject('ERROR C'),
;
]Promise.any(promises)
.catch((aggregateError) => assert.deepEqual(
.errors,
aggregateError'ERROR A', 'ERROR B', 'ERROR C']
[; ))
Promise.any()
與 Promise.all()
Promise.any()
和 Promise.all()
可以用兩種方式比較
Promise.all()
:第一個輸入拒絕會拒絕結果 Promise,或其完成值是包含輸入完成值的陣列。Promise.any()
:第一個輸入完成會完成結果 Promise,或其拒絕值是包含輸入拒絕值(在錯誤物件內)的陣列。Promise.all()
對所有完成感興趣。相反的情況(至少一個拒絕)會導致拒絕。Promise.any()
對第一個完成感興趣。相反的情況(只有拒絕)會導致拒絕。Promise.any()
與 Promise.race()
Promise.any()
和 Promise.race()
也相關,但感興趣的事情不同
Promise.race()
對解決感興趣。第一個解決的 Promise 會「獲勝」。換句話說:我們想知道第一個終止的非同步運算。Promise.any()
對完成感興趣。第一個完成的 Promise 會「獲勝」。換句話說:我們想知道第一個成功的非同步運算。.race()
的主要(相對罕見)使用案例是 Promise 超時。.any()
的使用案例較廣泛。我們接下來會探討它們。
Promise.any()
的使用案例如果我們有多個非同步運算,且我們只對第一個成功的運算有興趣,我們會使用 Promise.any()
。換句話說,我們讓這些運算互相競爭,並使用最快的運算。
以下程式碼示範在下載資源時會是什麼樣子
const resource = await Promise.any([
fetch('http://example.com/first.txt')
.then(response => response.text()),
fetch('http://example.com/second.txt')
.then(response => response.text()),
; ])
相同的模式讓我們可以使用下載速度最快的模組
const lodash = await Promise.any([
import('https://primary.example.com/lodash'),
import('https://secondary.example.com/lodash'),
; ])
為了比較,如果次要伺服器只是一個備援機制,在主要伺服器發生故障時使用,我們會使用以下程式碼
let lodash;
try {
= await import('https://primary.example.com/lodash');
lodash catch {
} = await import('https://secondary.example.com/lodash');
lodash }
Promise.any()
?Promise.any()
的一個簡單實作基本上是 Promise.all()
實作的鏡像版本。
Promise.allSettled()
[ES2020]這次,型別簽章稍微複雜一點。您可以直接跳到第一個示範,它應該比較容易理解。
這是 Promise.allSettled()
的型別簽章
Promise.allSettled<T>(promises: Iterable<Promise<T>>)
: Promise<Array<SettlementObject<T>>>
它會傳回一個 Promise,其中包含一個陣列,而這個陣列的元素具有以下型別簽章
type SettlementObject<T> = FulfillmentObject<T> | RejectionObject;
interface FulfillmentObject<T> {
: 'fulfilled';
status: T;
value
}
interface RejectionObject {
: 'rejected';
status: unknown;
reason }
Promise.allSettled()
會傳回一個 Promise out
。一旦所有 promises
都已解決,out
便會以一個陣列完成。該陣列的每個元素 e
都對應到 promises
的一個 Promise p
如果 p
以完成值 v
完成,則 e
是
status: 'fulfilled', value: v } {
如果 p
以拒絕值 r
拒絕,則 e
是
status: 'rejected', reason: r } {
除非在反覆運算 promises
時發生錯誤,否則輸出 Promise out
永遠不會被拒絕。
圖 26 說明了 Promise.allSettled()
的運作方式。
Promise.allSettled()
的第一個示範這是 Promise.allSettled()
運作方式的快速第一個示範
Promise.allSettled([
Promise.resolve('a'),
Promise.reject('b'),
]).then(arr => assert.deepEqual(arr, [
status: 'fulfilled', value: 'a' },
{ status: 'rejected', reason: 'b' },
{ ; ]))
Promise.allSettled()
的較長範例下一個範例類似於 .map()
加上 Promise.all()
的範例(我們從中借用 downloadText()
函式):我們正在下載多個文字檔,其 URL 儲存在一個陣列中。但是,這次我們不希望在發生錯誤時停止,我們想要繼續執行。Promise.allSettled()
讓我們可以做到這一點
const urls = [
'http://example.com/exists.txt',
'http://example.com/missing.txt',
;
]
const result = Promise.allSettled(
.map(u => downloadText(u)));
urls.then(
result=> assert.deepEqual(
arr ,
arr
[
{status: 'fulfilled',
value: 'Hello!',
,
}
{status: 'rejected',
reason: new Error('Not Found'),
,
}
]; ))
Promise.allSettled()
的一個簡單實作這是 Promise.allSettled()
的一個簡化實作(例如,它不執行任何安全檢查)
function allSettled(iterable) {
return new Promise((resolve, reject) => {
let elementCount = 0;
let result;
function addElementToResult(i, elem) {
= elem;
result[i] ++;
elementCountif (elementCount === result.length) {
resolve(result);
}
}
let index = 0;
for (const promise of iterable) {
// Capture the current value of `index`
const currentIndex = index;
.then(
promise=> addElementToResult(
(value) , {
currentIndexstatus: 'fulfilled',
value,
})=> addElementToResult(
(reason) , {
currentIndexstatus: 'rejected',
reason;
}))++;
index
}if (index === 0) {
resolve([]);
return;
}// Now we know how many Promises there are in `iterable`.
// We can wait until now with initializing `result` because
// the callbacks of .then() are executed asynchronously.
= new Array(index);
result ;
}) }
對於 Promise 組合器,短路表示輸出 Promise 會在所有輸入 Promise 都已解決之前提早解決。以下組合器會短路
Promise.all()
:只要一個輸入 Promise 被拒絕,輸出 Promise 就會被拒絕。Promise.race()
:只要一個輸入 Promise 被解決,輸出 Promise 就會被解決。Promise.any()
:只要一個輸入 Promise 被完成,輸出 Promise 就會被完成。再次強調,提早解決並不表示會停止被忽略 Promise 背後的運算。這只表示會忽略它們的解決。
Promise.all()
(進階)考慮以下程式碼
const asyncFunc1 = () => Promise.resolve('one');
const asyncFunc2 = () => Promise.resolve('two');
asyncFunc1()
.then(result1 => {
.equal(result1, 'one');
assertreturn asyncFunc2();
}).then(result2 => {
.equal(result2, 'two');
assert; })
使用 .then()
以這種方式執行 Promise 為基礎的函式會順序地執行:只有在 asyncFunc1()
的結果被解決後,asyncFunc2()
才會被執行。
Promise.all()
有助於更並行地執行 Promise 為基礎的函式
Promise.all([asyncFunc1(), asyncFunc2()])
.then(arr => {
.deepEqual(arr, ['one', 'two']);
assert; })
判斷非同步程式碼「並行」程度的提示:專注於非同步運算開始的時間,而不是它們的 Promise 如何被處理。
例如,以下每個函式都會並行執行 asyncFunc1()
和 asyncFunc2()
,因為它們幾乎在同一時間開始。
function concurrentAll() {
return Promise.all([asyncFunc1(), asyncFunc2()]);
}
function concurrentThen() {
const p1 = asyncFunc1();
const p2 = asyncFunc2();
return p1.then(r1 => p2.then(r2 => [r1, r2]));
}
另一方面,以下兩個函式都會順序執行 asyncFunc1()
和 asyncFunc2()
:asyncFunc2()
只有在 asyncFunc1()
的 Promise 被完成後才會被呼叫。
function sequentialThen() {
return asyncFunc1()
.then(r1 => asyncFunc2()
.then(r2 => [r1, r2]));
}
function sequentialAll() {
const p1 = asyncFunc1();
const p2 = p1.then(() => asyncFunc2());
return Promise.all([p1, p2]);
}
Promise.all()
是 fork-joinPromise.all()
與並行模式「fork join」有鬆散的關聯。讓我們重新檢視一個我們之前遇到的範例
Promise.all([
// (A) fork
downloadText('http://example.com/first.txt'),
downloadText('http://example.com/second.txt'),
])// (B) join
.then(
=> assert.deepEqual(
(arr) , ['First!', 'Second!']
arr; ))
本節提供串接 Promise 的提示。
問題
// Don’t do this
function foo() {
const promise = asyncFunc();
.then(result => {
promise// ···
;
})
return promise;
}
運算從 asyncFunc()
傳回的 Promise 開始。但是之後,運算會繼續,並透過 .then()
建立另一個 Promise。foo()
傳回前一個 Promise,但應該傳回後一個。以下是修正方法
function foo() {
const promise = asyncFunc();
return promise.then(result => {
// ···
;
}) }
問題
// Don’t do this
asyncFunc1()
.then(result1 => {
return asyncFunc2()
.then(result2 => { // (A)
// ···
;
}); })
A 行中的 .then()
是巢狀的。扁平結構會更好
asyncFunc1()
.then(result1 => {
return asyncFunc2();
}).then(result2 => {
// ···
; })
這是另一個可避免的巢狀範例
// Don’t do this
asyncFunc1()
.then(result1 => {
if (result1 < 0) {
return asyncFuncA()
.then(resultA => 'Result: ' + resultA);
else {
} return asyncFuncB()
.then(resultB => 'Result: ' + resultB);
}; })
我們可以再次取得扁平結構
asyncFunc1()
.then(result1 => {
return result1 < 0 ? asyncFuncA() : asyncFuncB();
}).then(resultAB => {
return 'Result: ' + resultAB;
; })
在以下程式碼中,我們實際上受益於巢狀
.open()
db.then(connection => { // (A)
return connection.select({ name: 'Jane' })
.then(result => { // (B)
// Process result
// Use `connection` to make more queries
})// ···
.finally(() => {
.close(); // (C)
connection;
}) })
我們在 A 行接收非同步結果。在 B 行,我們巢狀,以便我們可以在 C 行的回呼和程式碼中存取變數 connection
。
問題
// Don’t do this
class Model {
insertInto(db) {
return new Promise((resolve, reject) => { // (A)
.insert(this.fields)
db.then(resultCode => {
this.notifyObservers({event: 'created', model: this});
resolve(resultCode);
.catch(err => {
})reject(err);
});
})
}// ···
}
在 A 行,我們建立一個 Promise 來傳送 db.insert()
的結果。這是不必要的冗長,可以簡化
class Model {
insertInto(db) {
return db.insert(this.fields)
.then(resultCode => {
this.notifyObservers({event: 'created', model: this});
return resultCode;
;
})
}// ···
}
關鍵概念是我們不需要建立 Promise;我們可以傳回 .then()
呼叫的結果。另一個好處是我們不需要捕捉和重新拒絕 db.insert()
的失敗。我們只需將其拒絕傳遞給 .insertInto()
的呼叫者即可。
除非另有註明,否則此功能是在 ECMAScript 6 中引入的(當時將 Promises 加入語言中)。
詞彙表
Promise.all()
Promise.all<T>(promises: Iterable<Promise<T>>)
: Promise<Array<T>>
P
的完成:如果所有輸入 Promise 都已完成。
P
的拒絕:如果一個輸入 Promise 被拒絕。
Promise.race()
Promise.race<T>(promises: Iterable<Promise<T>>)
: Promise<T>
P
的解決:如果第一個輸入 Promise 已解決。
Promise.any()
[ES2021]Promise.any<T>(promises: Iterable<Promise<T>>): Promise<T>
P
的完成:如果一個輸入 Promise 已完成。
P
的拒絕:如果所有輸入 Promise 都被拒絕。
AggregateError
。這是 AggregateError
的類型簽章(省略了一些成員)
class AggregateError {
constructor(errors: Iterable<any>, message: string);
get errors(): Array<any>;
get message(): string;
}
Promise.allSettled()
[ES2020]Promise.allSettled<T>(promises: Iterable<Promise<T>>)
: Promise<Array<SettlementObject<T>>>
P
的完成:如果所有輸入 Promise 都已解決。
P
的拒絕:如果在反覆處理輸入 Promise 時發生錯誤。這是 SettlementObject
的類型簽章
type SettlementObject<T> = FulfillmentObject<T> | RejectionObject;
interface FulfillmentObject<T> {
: 'fulfilled';
status: T;
value
}
interface RejectionObject {
: 'rejected';
status: unknown;
reason }