Promise靜態四兄弟實現示例詳解
前言
恰逢 Promise
也有四個很像的靜態三兄弟(Promise.all
、Promise.allSettled
、Promise.race
、Promise.any
),它們接受的參數類型相同,但各自邏輯處理不同,它們具體會有什麼區別那?別急,下面等小包慢慢道來。
在文章的開始,小包先給大傢提出幾個問題:
Promise.all
與Promise.allSettled
有啥區別啊?Promise.race
的運行機制?Promise.any
吶,兩者有啥區別?- 四兄弟隻能接受數組作為參數嗎?
- 四兄弟方法我們應該如何優雅完美的實現?
Promise.all
Promise.all
在目前手寫題中熱度頻度應該是 top5
級別的,所以我們要深刻掌握 Promise.all
方法。下面首先來簡單回顧一下 all
方法。
基礎學習
Promise.all
方法類似於一群兄弟們並肩前行,參數可以類比為一群兄弟,隻有當兄弟全部快樂,all
老大才會收獲快樂;隻要有一個兄弟不快樂,老大就不會快樂。
Promise.all()
方法用於將多個 Promise
實例,包裝成一個新的 Promise
實例。
const p = Promise.all([p1, p2, p3]);
Promise.all
方法接受一個數組做參數,p1、p2、p3
都是 Promise
實例。如果不是 Promise
實例,則會先調用 Promise.resolve
方法將參數先轉化為 Promise
實例,之後進行下一步處理。
返回值 p 的狀態由 p1、p2、p3 決定,可以分成兩種情況:
- 隻有
p1、p2、p3
的狀態都變成fulfilled
,p
的狀態才會變成fulfilled
,此時p1、p2、p3
的返回值組成一個數組,傳遞給p
的回調函數。 - 隻要
p1、p2、p3
之中有一個被rejected
,p
的狀態就變成rejected
,此時第一個被reject
的實例的返回值,會傳遞給p
的回調函數。
// 模擬異步的promise const p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve(1); }, 1000); }); // 普通promise const p2 = Promise.resolve(2); // 常數值 const p3 = 3; // 失敗的promise const p4 = Promise.reject("error"); // 異步失敗的promise const p5 = new Promise((resolve, reject) => { setTimeout(() => { reject("TypeError"); }, 1000); }); // 1. promise全部成功 Promise.all([p1, p2, p3]) .then((data) => console.log(data)) // [1, 2, 3] .catch((error) => console.log(error)); // 2. 存在失敗的promise Promise.all([p1, p2, p3, p4]) .then((data) => console.log(data)) .catch((error) => console.log(error)); // error // 3. 存在多個失敗的promise Promise.all([p1, p2, p3, p4, p5]) .then((data) => console.log(data)) .catch((error) => console.log(error)); // error
從上面案例的輸出中,我們可以得出下列結論:
p
狀態由參數執行結果決定,全部成功則返回成功,存有一個失敗則失敗- 參數為非
Promise
實例,會通過Promise.resolve
轉化成Promise
實例 - 成功後返回一個數組,數組內數據按照參數順序排列
- 短路效應: 隻會返回第一個失敗信息
Iterator 接口參數
《ES6 入門教程》還指出: Promise.all 方法可以不是數組,但必須具有 Iterator 接口,且返回的每個成員都是 Promise 實例
說實話,加粗部分小包是沒能完全理解的,難道 Promise.all
使用 Iterator
類型時,要求迭代項都是 Promise
實例嗎?我們以 String
類型為例,看 Promise.all
是否可以支持迭代項為非 Promise
實例。
// ['x', 'i', 'a', 'o', 'b', 'a', 'o'] Promise.all("xiaobao").then((data) => console.log(data));
可見 Promise
對 Iterator
類型的處理與數組相同,如果參數不是 Promise
實例,會先調用 Promise.all
轉化為 Promise
實例。
思路分析
Promise.all
會返回一個新Promise
對象
Promise.all = function (promises) { return new Promise((resolve, reject) => {}); };
- (亮點)
all
方法參數可以是數組,同樣也可以是Iterator
類型,因此應該使用for of
循環進行遍歷。
Promise.all = function (promises) { return new Promise((resolve, reject) => { for (let p of promises) { } }); };
- 某些參數有可能未必是
Promise
類型,因此參數使用前先通過Promise.resolve
轉換
Promise.all = function (promises) { return new Promise((resolve, reject) => { for (let p of promises) { // 保證所有的參數為 promise 實例,然後執行後續操作 Promise.resolve(p).then((data) => { //... }); } }); };
Iterator
類型我們是無法得知迭代深度,因此我們要維護一個 count
用來記錄 promise
總數,同時維護 fulfilledCount
代表完成的 promise
數,當 count === fulfilledCount
,代表所有傳入的 Promise
執行成功,返回數據。
Promise.all = function (promises) { let count = 0; // promise總數 let fulfilledCount = 0; // 完成的promise數 return new Promise((resolve, reject) => { for (let p of promises) { count++; // promise總數 + 1 Promise.resolve(p).then((data) => { fulfilledCount++; // 完成的promise數量+1 if (count === fulfilledCount) { // 代表最後一個promise完成瞭 resolve(); } }); } }); };
有可能有的讀者會好奇,為啥 count === fulfilledCount 可以判斷所有的 promise 都完成瞭吶?
Promise.then
方法是 microTasks
(微任務),當同步任務執行完畢後,Event Loop
才會去執行 microTasks
。count++
位於同步代碼部分,因此在執行 promise.then
方法之前,已經成功的計算出 promise
的總數。
然後依次執行 promise.then
方法,fulfilledCount
增加,當 count === fulfilledCount
說明所有的 promise
都已經成功完成瞭。
返回數據的順序應該是 all
方法中比較難處理的部分。
- 創建一個數組
result
存儲所有promise
成功的數據 - 在
for of
循環中,使用let
變量定義i
,其值等於當前的遍歷索引 let
定義的變量不會發生變量提升,因此我們直接令result[i]
為promise
成功數據,這樣就可以實現按參數輸入順序輸出結果
Promise.all = function (promises) { const result = []; // 存儲promise成功數據 let count = 0; let fulfilledCount = 0; return new Promise((resolve, reject) => { for (let p of promises) { // i為遍歷的第幾個promise // 使用let避免形成閉包問題 let i = count; count++; // 保證所有的參數為 promise 實例,然後執行後續操作 Promise.resolve(p).then((data) => { fulfilledCount++; // 將第i個promise成功數據賦值給對應位置 result[i] = data; if (count === fulfilledCount) { // 代表最後一個promise完成瞭 // 返回result數組 resolve(result); } }); } }); };
處理一下邊界情況
- 某個
promise
失敗——直接調用reject
即可 - 傳入
promise
數量為0
——返回空數組(規范規定) - 代碼執行過程拋出異常 —— 返回錯誤信息
// 多餘代碼省略 Promise.all = function (promises) { return new Promise((resolve, reject) => { // 3.捕獲代碼執行中的異常 try{ for (let p of promises) { Promise.resolve(p).then(data => {} .catch(reject); // 1.直接調用reject函數返回失敗原因 }) } // 2.傳入promise數量為0 if (count === 0) { resolve(result) } } catch(error) { reject(error) } }) }
源碼實現
我們把上面的代碼匯總一下,加上詳細的註釋,同時測試一下手寫 Promise.all
是否成功。
Promise.all = function (promises) { const result = []; // 存儲promise成功數據 let count = 0; // promise總數 let fulfilledCount = 0; //完成promise數量 return new Promise((resolve, reject) => { // 捕獲代碼執行中的異常 try { for (let p of promises) { // i為遍歷的第幾個promise // 使用let避免形成閉包問題 let i = count; count++; // promise總數 + 1 Promise.resolve(p) .then((data) => { fulfilledCount++; // 完成的promise數量+1 // 將第i個promise成功數據賦值給對應位置 result[i] = data; if (count === fulfilledCount) { // 代表最後一個promise完成瞭 // 返回result數組 resolve(result); } }) .catch(reject); // 傳入promise數量為0 if (count === 0) { resolve(result); // 返回空數組 } } } catch (error) { reject(error); } }); };
測試代碼(使用案例中的測試代碼,附加 Iterator
類型 Stirng
):
// 1. promise全部成功 Promise.all([p1, p2, p3]) .then((data) => console.log(data)) // [1, 2, 3] .catch((error) => console.log(error)); // 2. 存在失敗的promise Promise.all([p1, p2, p3, p4]) .then((data) => console.log(data)) .catch((error) => console.log(error)); // error // 3. 存在多個失敗的promise Promise.all([p1, p2, p3, p4, p5]) .then((data) => console.log(data)) .catch((error) => console.log(error)); // error // 4. String 類型 Promise.all("zcxiaobao").then((data) => console.log(data)); // ['z', 'c', 'x', 'i', 'a', 'o', 'b', 'a', 'o']
Promise.allSettled
基礎學習
不是每群兄弟們都會碰到好老大(all
方法),allSettled
方法他並不管兄弟們的死活,他隻管兄弟們是否做瞭,而他的任務就是把所有兄弟的結果返回。
Promise.allSettled()
方法接受一個數組作為參數,數組的每個成員都是一個 Promise
對象,並返回一個新的 Promise
對象。隻有等到參數數組的所有 Promise
對象都發生狀態變更(不管是 fulfilled
還是 rejected
),返回的 Promise
對象才會發生狀態變更。
還是以上面的例子為例,我們來看一下與 Promise.all
方法有啥不同。
// 1. promise 全部成功 Promise.allSettled([p1, p2, p3]) .then((data) => console.log(data)) // [1, 2, 3] .catch((error) => console.log(error)); // 2. 存在失敗的 promise Promise.allSettled([p1, p2, p3, p4]) .then((data) => console.log(data)) .catch((error) => console.log(error)); // error // 3. 存在多個失敗的 promise Promise.allSettled([p1, p2, p3, p4, p5]) .then((data) => console.log(data)) .catch((error) => console.log(error)); // error // 4. 傳入 String 類型 Promise.allSettled("zc").then((data) => console.log(data));
從輸出結果我們可以發現:
allSettled
方法隻會成功,不會失敗- 返回結果每個成員為對象,對象的格式固定
- 如果
promise
成功,對象屬性值status: fulfilled
,value
記錄成功值 - 如果 promise 失敗,對象屬性值
status: rejected
,reason
記錄失敗原因。
- 如果
allSettled
方法也可以接受Iterator
類型參數
思路分析
allSettled
方法與 all
方法最大的區別在於兩點:
allSettled
方法沒有失敗情況allSettled
方法返回有固定格式
我們可以圍繞這兩點改造 all
方法。
all
方法我們是通過計算成功數量來判斷是否終結,allSettled
方法不計較成功失敗,因此我們需要計算成功/失敗總數量即可。
在累加完成總數量的過程中,分情況構造 allSettled
所需要的數據格式: 成功時壓入成功格式,失敗時壓入失敗格式。
源碼實現
由於有瞭 all
方法手寫的基礎,上面就不一步一步囉嗦的實現瞭。
Promise.allSettled = function (promises) { const result = []; let count = 0; let totalCount = 0; //完成promise數量 return new Promise((resolve, reject) => { try { for (let p of promises) { let i = count; count++; // promise總數 + 1 Promise.resolve(p) .then((res) => { totalCount++; // 成功時返回成功格式數據 result[i] = { status: "fulfilled", value: res, }; // 執行完成 if (count === totalCount) { resolve(result); } }) .catch((error) => { totalCount++; // 失敗時返回失敗格式數據 result[i] = { status: "rejected", reason: error, }; // 執行完成 if (count === totalCount) { resolve(result); } }); if (count === 0) { resolve(result); } } } catch (error) { reject(error); } }); };
Promise.race
基礎學習
race
方法形象化來講就是賽跑機制,隻認第一名,不管是成功的第一還是失敗的第一。
Promise.race()
方法同樣是接收多個 Promise
實例,包裝成一個新的 Promise
實例。
const p = Promise.race([p1, p2, p3]);
上面案例中,隻要 p1、p2、p3
之中有一個實例率先改變狀態,p
的狀態就跟著改變。那個率先改變的 Promise
實例的返回值,就傳遞給 p
的回調函數。
const p1 = new Promise((resolve, reject) => { setTimeout(()=> { resolve(1) },1000) }) const p2 = new Promise((resolve, reject) => { setTimeout(()=> { reject(2) },2000) }) const p3 = 3; // 成功在先,失敗在後 Promise.race([p1, p2]).then(res => {console.log(res)}) // 1 // 同步在先,異步在後 Promise.race([p1, p3]).then(res => console.log(res)) // 3 // String Promise.race('zc').then(res => console.log(res)) // z
思路分析
race
方法就沒有那麼多彎彎繞繞瞭,隻要某個 promise
改變狀態就返回其對應結果。
因此我們隻需監聽每個 promise
的 then
與 catch
方法,當發生狀態改變,直接調用 resolve
和 reject
方法即可。
源碼實現
Promise.race(promises) { return new Promise((resolve, reject) => { for (let p of promises) { // Promise.resolve將p進行轉化,防止傳入非Promise實例 // race執行機制為那個實例發生狀態改變,則返回其對應結果 // 因此監聽 Promise.resolve(p).then(resolve).catch(reject); } }) }
Promise.any
基礎學習
any 方法形象化來說是天選唯一,隻要第一個成功者。如果全部失敗瞭,就返回失敗情況。
ES2021
引入瞭 Promise.any()
方法。該方法接受一組 Promise
實例作為參數,包裝成一個新的 Promise
實例返回。
any
方法與 race
方法很像,也存在短路特性,隻要有一個實例變成 fulfilled
狀態,就會返回成功的結果;如果全部失敗,則返回失敗情況。
// 成功的promise const p1 = new Promise((resolve, reject) => { setTimeout(()=> { resolve(1) },1000) }) // 失敗的promise const p2 = new Promise((resolve, reject) => { setTimeout(()=> { reject(2) },2000) }) //失敗的promise const p3 = new Promise((resolve, reject) => { reject(3) }) // 存在一個成功的promise Promise.any([p1,p2]).then(res => console.log(res))// 1 // 全部失敗的promise Promise.any([p2,p3]).then(res => console.log(res)) .catch(error => console.log(error)) // AggregateError: All promises were rejected // String類型 Promise.any('zc').then(res => console.log(res)) // z
通過上述輸出結果我們可以發現:
any
方法也可以接受Iterator
格式參數- 當一個
promise
實例轉變為fulfilled
時,any
返回成功的promise
,值為最早成功的promise
值。 - 當
promise
全部失敗時,any
返回失敗的promise
,值固定為 AggregateError: All promises were rejected
思路分析
上面我們分析瞭 any
方法的機制:
- 某個實例轉化為
fulfilled
,any
隨之返回成功的promise
。因此這裡我們就可以類似使用race
的方法,監測每個promise
的成功。 - 全部實例轉化為
rejected
,any
返回AggregateError: All promises were rejected
。這裡我們可以參考all
方法的全部成功,才返回成功,因此我們需要累計失敗數量,當rejectCount === count
時,返回失敗值。
源碼實現
Promise.any = function(promises) { return new Promise((resolve,reject) => { let count = 0; let rejectCount = 0; let errors = []; let i = 0; for (let p of promises) { i = count; count ++; Promise.resolve(p).then(res => { resolve(res) }).catch(error => { errors[i] = error; rejectCount ++; if (rejectCount === count) { return reject(new AggregateError(errors)) } }) } if(count === 0) return reject(new AggregateError('All promises were rejected')) }) }
以上就是Promise靜態四兄弟實現示例詳解的詳細內容,更多關於Promise靜態實現的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- 字節飛書面試promise.all實現示例
- 面試手寫實現Promise.all
- 前端JavaScript之Promise
- 徹底搞懂 javascript的Promise
- JS Promise axios 請求結果後面的.then() 是什麼意思