摘要: 玩轉(zhuǎn)Promise。
- 原文:Promise 中的三兄弟 .all(), .race(), .allSettled()
- 譯者:前端小智
Fundebug經(jīng)授權(quán)轉(zhuǎn)載诲宇,版權(quán)歸原作者所有。
從ES6 開始惶翻,我們大都使用的是 Promise.all()
和Promise.race()
姑蓝,Promise.allSettled()
提案已經(jīng)到第4階段,因此將會(huì)成為ECMAScript 2020
的一部分吕粗。
1.概述
Promise.all<T>(promises: Iterable<Promise<T>>): Promise<Array<T>>
-
Promise.all(iterable)
方法返回一個(gè)Promise
實(shí)例它掂,此實(shí)例在iterable
參數(shù)內(nèi)所有的promise
都“完成(resolved)”或參數(shù)中不包含promise
時(shí)回調(diào)完成(resolve);如果參數(shù)中promise
有一個(gè)失敗(rejected)虐秋,此實(shí)例回調(diào)失旈偶搿(reject),失敗原因的是第一個(gè)失敗promise
的結(jié)果
Promise.race<T>(promises: Iterable<Promise<T>>): Promise<T>
-
Promise.race(iterable) 方法返回一個(gè)
promise
客给,一旦迭代器中的某個(gè)promise
解決或拒絕用押,返回的promise
就會(huì)解決或拒絕。
Promise.allSettled<T>(promises: Iterable<Promise<T>>): Promise<Array<SettlementObject<T>>>
-
Promise.allSettled()方法返回一個(gè)
promise
靶剑,該promise
在所有給定的promise
已被解析或被拒絕后解析蜻拨,并且每個(gè)對(duì)象都描述每個(gè)promise
的結(jié)果。
2. 回顧: Promise 狀態(tài)
給定一個(gè)返回Promise
的異步操作桩引,以下這些是Promise
的可能狀態(tài):
- pending: 初始狀態(tài)缎讼,既不是成功,也不是失敗狀態(tài)坑匠。
- fulfilled: 意味著操作成功完成血崭。
- rejected: 意味著操作失敗。
- Settled:
Promise
要么被完成厘灼,要么被拒絕夹纫。Promise
一旦達(dá)成,它的狀態(tài)就不再改變设凹。
3.什么是組合
又稱部分-整體模式舰讹,將對(duì)象整合成樹形結(jié)構(gòu)以表示“部分整體”的層次結(jié)構(gòu)。組合模式使得用戶對(duì)單個(gè)對(duì)象和組合對(duì)象的使用具有一致性闪朱,它基于兩種函數(shù):
- 基元函數(shù)(簡(jiǎn)短:基元)創(chuàng)建原子塊月匣。
- 組合函數(shù)(簡(jiǎn)稱:組合)將原子和/或復(fù)合件組合在一起以形成復(fù)合件。
對(duì)于 JS 的 Promises 來說
- 基元函數(shù)包括:
Promise.resolve()
奋姿、Promise.reject()
- 組合函數(shù):
Promise.all()
,Promise.race()
,Promise.allSettled()
4. Promise.all()
Promise.all()
的類型簽名:
- Promise.all<T>(promises: Iterable<Promise<T>>): Promise<Array<T>>
返回情況:
完成(Fulfillment):
如果傳入的可迭代對(duì)象為空没酣,Promise.all
會(huì)同步地返回一個(gè)已完成(resolved
)狀態(tài)的promise
停团。
如果所有傳入的 promise
都變?yōu)橥瓿蔂顟B(tài),或者傳入的可迭代對(duì)象內(nèi)沒有 promise
,Promise.all
返回的 promise
異步地變?yōu)橥瓿伞?br>
在任何情況下唱遭,Promise.all
返回的 promise
的完成狀態(tài)的結(jié)果都是一個(gè)數(shù)組罗心,它包含所有的傳入迭代參數(shù)對(duì)象的值(也包括非 promise 值)电抚。
失敗/拒絕(Rejection):
如果傳入的 promise
中有一個(gè)失敶垦堋(rejected
),Promise.all
異步地將失敗的那個(gè)結(jié)果給失敗狀態(tài)的回調(diào)函數(shù)再榄,而不管其它 promise
是否完成狡刘。
來個(gè)例子:
const promises = [
Promise.resolve('a'),
Promise.resolve('b'),
Promise.resolve('c'),
];
Promise.all(promises)
.then((arr) => assert.deepEqual(
arr, ['a', 'b', 'c']
));
如果其中的一個(gè) promise 被拒絕,那么又是什么情況:
const promises = [
Promise.resolve('a'),
Promise.resolve('b'),
Promise.reject('ERROR'),
];
Promise.all(promises)
.catch((err) => assert.equal(
err, 'ERROR'
));
下圖說明Promise.all()
是如何工作的
4.1 異步 .map() 與 Promise.all()
數(shù)組轉(zhuǎn)換方法困鸥,如.map()
嗅蔬、.filter()
等剑按,用于同步計(jì)算。例如
function timesTwoSync(x) {
return 2 * x;
}
const arr = [1, 2, 3];
const result = arr.map(timesTwoSync);
assert.deepEqual(result, [2, 4, 6]);
如果.map()
的回調(diào)是基于Promise
的函數(shù)會(huì)發(fā)生什么澜术? 使用這種方式 .map()
返回的的結(jié)果是一個(gè)Promises
數(shù)組艺蝴。
Promises
數(shù)組不是普通代碼可以使用的數(shù)據(jù),但我們可以通過Promise.all()
來解決這個(gè)問題:它將Promises數(shù)組轉(zhuǎn)換為Promise
鸟废,并使用一組普通值數(shù)組來實(shí)現(xiàn)猜敢。
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 => {
assert.deepEqual(result, [2, 4, 6]);
});
更實(shí)際工作上關(guān)于 .map()示例
接下來,咱們使用.map()
和Promise.all()
從Web
下載文件盒延。 首先缩擂,咱們需要以下幫助函數(shù):
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 以字符串流的方式下載文件:
- 首先,它異步檢索響應(yīng)(第A行)添寺。
- response.ok(B行)檢查是否存在“找不到文件”等錯(cuò)誤胯盯。
- 如果沒有錯(cuò)誤,使用
.text()
(第C行)以字符串的形式取回文件的內(nèi)容计露。
在下面的示例中博脑,咱們 下載了兩個(gè)文件
const urls = [
'http://example.com/first.txt',
'http://example.com/second.txt',
];
const promises = urls.map(
url => downloadText(url));
Promise.all(promises)
.then(
(arr) => assert.deepEqual(
arr, ['First!', 'Second!']
));
Promise.all()的一個(gè)簡(jiǎn)版實(shí)現(xiàn)
function all(iterable) {
return new Promise((resolve, reject) => {
let index = 0;
for (const promise of iterable) {
// Capture the current value of `index`
const currentIndex = index;
promise.then(
(value) => {
if (anErrorOccurred) return;
result[currentIndex] = value;
elementCount++;
if (elementCount === result.length) {
resolve(result);
}
},
(err) => {
if (anErrorOccurred) return;
anErrorOccurred = true;
reject(err);
});
index++;
}
if (index === 0) {
resolve([]);
return;
}
let elementCount = 0;
let anErrorOccurred = false;
const result = new Array(index);
});
}
5. Promise.race()
Promise.race()
方法的定義:
Promise.race<T>(promises: Iterable<Promise<T>>): Promise<T>
Promise.race(iterable) 方法返回一個(gè) promise
,一旦迭代器中的某個(gè)promise
解決或拒絕薄坏,返回的 promise
就會(huì)解決或拒絕趋厉。來幾個(gè)例子寨闹,瞧瞧:
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'));
在第 A
行胶坠,Promise
是完成狀態(tài) ,所以 第 C
行會(huì)執(zhí)行(盡管第 B
行被拒絕)繁堡。
如果 Promise 被拒絕首先執(zhí)行沈善,在來看看情況是嘛樣的:
const promises = [
new Promise((resolve, reject) =>
setTimeout(() => resolve('result'), 200)),
new Promise((resolve, reject) =>
setTimeout(() => reject('ERROR'), 100)),
];
Promise.race(promises)
.then(
(result) => assert.fail(),
(err) => assert.equal(
err, 'ERROR'));
注意,由于 Promse
先被拒絕椭蹄,所以 Promise.race()
返回的是一個(gè)被拒絕的 Promise
這意味著Promise.race([])
的結(jié)果永遠(yuǎn)不會(huì)完成闻牡。
下圖演示了Promise.race()
的工作原理:
Promise.race() 在 Promise 超時(shí)下的情況
在本節(jié)中,我們將使用Promise.race()
來處理超時(shí)的 Promise
绳矩。 以下輔助函數(shù):
function resolveAfter(ms, value=undefined) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(value), ms);
});
}
resolveAfter()
主要做的是在指定的時(shí)間內(nèi)罩润,返回一個(gè)狀態(tài)為 resolve
的 Promise
,值為為傳入的 value
調(diào)用上面方法:
function timeout(timeoutInMs, promise) {
return Promise.race([
promise,
resolveAfter(timeoutInMs,
Promise.reject(new Error('Operation timed out'))),
]);
}
timeout()
返回一個(gè)Promise
翼馆,該 Promise
的狀態(tài)取決于傳入 promise
狀態(tài) 割以。
其中 timeout
函數(shù)中的 resolveAfter(timeoutInMs, Promise.reject(new Error('Operation timed out'))
,通過 resolveAfter
定義可知应媚,該結(jié)果返回的是一個(gè)被拒絕狀態(tài)的 Promise
严沥。
再來看看timeout(timeoutInMs, promise)
的運(yùn)行情況。如果傳入promise
在指定的時(shí)間之前狀態(tài)為完成時(shí)中姜,timeout
返回結(jié)果就是一個(gè)完成狀態(tài)的 Promise
,可以通過.then
的第一個(gè)回調(diào)參數(shù)處理返回的結(jié)果消玄。
timeout(200, resolveAfter(100, 'Result!'))
.then(result => assert.equal(result, 'Result!'));
相反,如果是在指定的時(shí)間之后完成,剛 timeout
返回結(jié)果就是一個(gè)拒絕狀態(tài)的 Promise
,從而觸發(fā)catch
方法指定的回調(diào)函數(shù)翩瓜。
timeout(100, resolveAfter(2000, 'Result!'))
.catch(err => assert.deepEqual(err, new Error('Operation timed out')));
重要的是要了解“Promise 超時(shí)”的真正含義:
- 如果傳入入
Promise
較到的得到解決受扳,其結(jié)果就會(huì)給返回的Promise
。 - 如果沒有足夠快得到解決兔跌,輸出的
Promise
的狀態(tài)為拒絕辞色。
也就是說,超時(shí)只會(huì)阻止傳入的Promise浮定,影響輸出 Promise(因?yàn)镻romise只能解決一次)相满, 但它并沒有阻止傳入Promise
的異步操作。
5.2 Promise.race() 的一個(gè)簡(jiǎn)版實(shí)現(xiàn)
以下是 Promise.race()
的一個(gè)簡(jiǎn)化實(shí)現(xiàn)(它不執(zhí)行安全檢查)
function race(iterable) {
return new Promise((resolve, reject) => {
for (const promise of iterable) {
promise.then(
(value) => {
if (settlementOccurred) return;
settlementOccurred = true;
resolve(value);
},
(err) => {
if (settlementOccurred) return;
settlementOccurred = true;
reject(err);
});
}
let settlementOccurred = false;
});
}
6.Promise.allSettled()
“Promise.allSettled”
這一特性是由Jason Williams桦卒,Robert Pamely和Mathias Bynens提出立美。
promise.allsettle()
方法的定義:
-
Promise.allSettled<T>(promises: Iterable<Promise<T>>)
: Promise<Array<SettlementObject<T>>>
它返回一個(gè)Array
的Promise
,其元素具有以下類型特征:
type SettlementObject<T> = FulfillmentObject<T> | RejectionObject;
interface FulfillmentObject<T> {
status: 'fulfilled';
value: T;
}
interface RejectionObject {
status: 'rejected';
reason: unknown;
}
Promise.allSettled()
方法返回一個(gè)promise方灾,該promise在所有給定的promise已被解析或被拒絕后解析建蹄,并且每個(gè)對(duì)象都描述每個(gè)promise的結(jié)果。
舉例說明, 比如各位用戶在頁面上面同時(shí)填了3個(gè)獨(dú)立的表單, 這三個(gè)表單分三個(gè)接口提交到后端, 三個(gè)接口獨(dú)立, 沒有順序依賴, 這個(gè)時(shí)候我們需要等到請(qǐng)求全部完成后給與用戶提示表單提交的情況
在多個(gè)promise
同時(shí)進(jìn)行時(shí)咱們很快會(huì)想到使用Promise.all
來進(jìn)行包裝, 但是由于Promise.all
的短路特性, 三個(gè)提交中若前面任意一個(gè)提交失敗, 則后面的表單也不會(huì)進(jìn)行提交了, 這就與咱們需求不符合.
Promise.allSettled
跟Promise.all
類似, 其參數(shù)接受一個(gè)Promise
的數(shù)組, 返回一個(gè)新的Promise
, 唯一的不同在于, 其不會(huì)進(jìn)行短路, 也就是說當(dāng)Promise
全部處理完成后我們可以拿到每個(gè)Promise
的狀態(tài), 而不管其是否處理成功.
下圖說明promise.allsettle()
是如何工作的
6.1 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' },
]));
6.2 Promise.allSettled() 較復(fù)雜點(diǎn)的例子
這個(gè)示例類似于.map()
和Promise.all()
示例(我們從其中借用了downloadText()
函數(shù)):我們下載多個(gè)文本文件裕偿,這些文件的url
存儲(chǔ)在一個(gè)數(shù)組中洞慎。但是,這一次嘿棘,咱們不希望在出現(xiàn)錯(cuò)誤時(shí)停止劲腿,而是希望繼續(xù)執(zhí)行。Promise.allSettled()
允許咱們這樣做:
const urls = [
'http://example.com/exists.txt',
'http://example.com/missing.txt',
];
const result = Promise.allSettled(
urls.map(u => downloadText(u)));
result.then(
arr => assert.deepEqual(
arr,
[
{
status: 'fulfilled',
value: 'Hello!',
},
{
status: 'rejected',
reason: new Error('Not Found'),
},
]
));
6.3 Promise.allSettled() 的簡(jiǎn)化實(shí)現(xiàn)
這是promise.allsettle()
的簡(jiǎn)化實(shí)現(xiàn)(不執(zhí)行安全檢查)
function allSettled(iterable) {
return new Promise((resolve, reject) => {
function addElementToResult(i, elem) {
result[i] = elem;
elementCount++;
if (elementCount === result.length) {
resolve(result);
}
}
let index = 0;
for (const promise of iterable) {
// Capture the current value of `index`
const currentIndex = index;
promise.then(
(value) => addElementToResult(
currentIndex, {
status: 'fulfilled',
value
}),
(reason) => addElementToResult(
currentIndex, {
status: 'rejected',
reason
}));
index++;
}
if (index === 0) {
resolve([]);
return;
}
let elementCount = 0;
const result = new Array(index);
});
}
7. 短路特性
Promise.all()
和 romise.race()
都具有 短路特性
-
Promise.all(): 如果參數(shù)中
promise
有一個(gè)失斈衩睢(rejected)焦人,此實(shí)例回調(diào)失敗(reject)
Promise.race():如果參數(shù)中某個(gè)promise
解決或拒絕重父,返回的 promise就會(huì)解決或拒絕花椭。
8.并發(fā)性和 Promise.all()
8.1 順序執(zhí)行與并發(fā)執(zhí)行
考慮下面的代碼:
asyncFunc1()
.then(result1 => {
assert.equal(result1, 'one');
return asyncFunc2();
})
.then(result2 => {
assert.equal(result2, 'two');
});
使用.then()
順序執(zhí)行基于Promise
的函數(shù):只有在 asyncFunc1()
的結(jié)果被解決后才會(huì)執(zhí)行asyncFunc2()
。
而 Promise.all()
是并發(fā)執(zhí)行的
Promise.all([asyncFunc1(), asyncFunc2()])
.then(arr => {
assert.deepEqual(arr, ['one', 'two']);
});
9.2 并發(fā)技巧:關(guān)注操作何時(shí)開始
確定并發(fā)異步代碼的技巧:關(guān)注異步操作何時(shí)啟動(dòng)房午,而不是如何處理它們的Promises矿辽。
例如,下面的每個(gè)函數(shù)都同時(shí)執(zhí)行asyncFunc1()
和asyncFunc2()
郭厌,因?yàn)樗鼈儙缀跬瑫r(shí)啟動(dòng)袋倔。
function concurrentAll() {
return Promise.all([asyncFunc1(), asyncFunc2()]);
}
function concurrentThen() {
const p1 = asyncFunc1();
const p2 = asyncFunc2();
return p1.then(r1 => p2.then(r2 => [r1, r2]));
}
另一方面,以下兩個(gè)函數(shù)依次執(zhí)行asyncFunc1()
和asyncFunc2()
: asyncFunc2()
僅在asyncFunc1()
的解決之后才調(diào)用沪曙。
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]);
}
9.3 Promise.all() 與 Fork-Join 分治編程
Promise.all()
與并發(fā)模式“fork join”松散相關(guān)奕污。重溫一下咱們前面的一個(gè)例子:
Promise.all([
// (A) fork
downloadText('http://example.com/first.txt'),
downloadText('http://example.com/second.txt'),
])
// (B) join
.then(
(arr) => assert.deepEqual(
arr, ['First!', 'Second!']
));
- Fork:在
A
行中,分割兩個(gè)異步任務(wù)并同時(shí)執(zhí)行它們液走。 - Join:在
B
行中碳默,對(duì)每個(gè)小任務(wù)得到的結(jié)果進(jìn)行匯總贾陷。
代碼部署后可能存在的BUG沒法實(shí)時(shí)知道,事后為了解決這些BUG嘱根,花了大量的時(shí)間進(jìn)行l(wèi)og 調(diào)試髓废,這邊順便給大家推薦一個(gè)好用的BUG監(jiān)控工具 Fundebug。
原文:https://2ality.com/2019/08/promise-combinators.html
關(guān)于Fundebug
Fundebug專注于JavaScript该抒、微信小程序慌洪、微信小游戲、支付寶小程序凑保、React Native冈爹、Node.js和Java線上應(yīng)用實(shí)時(shí)BUG監(jiān)控。 自從2016年雙十一正式上線欧引,F(xiàn)undebug累計(jì)處理了20億+錯(cuò)誤事件频伤,付費(fèi)客戶有陽光保險(xiǎn)、核桃編程芝此、荔枝FM憋肖、掌門1對(duì)1、微脈婚苹、青團(tuán)社等眾多品牌企業(yè)岸更。歡迎大家免費(fèi)試用!