Promise.all 是什麼?請實現 Promise.all
2023年12月3日
💎 加入 E+ 成長計畫 如果你喜歡我們的內容,歡迎加入 E+,獲得更多深入的軟體前後端內容
作為前端工程師,在日常的開發中很常用到 Promise.all()
,然而你知道該怎麼實現這個方法嗎? 這是面試很常出現的問題,很多人因為在面試時寫不出來而被刷掉,如果你還不知道怎麼實現的話,就讓我們透過這篇文章一步步實現。
Promise.all() 是什麼?
要實現這個方法前,我們要先知道它在做什麼。根據 MDN 的定義,Promise.all()
會
- 接收一個內有多個 promises 的
Iterable
,例如 Array、Map、Set。 - 如果
Iterable
是空的,例如空 Array,則 fulfilled 值會是空的 Array。 - 如果
Iterable
不是空的,則如果所有的 promises 都 fulfilled,則依序回傳 fulfilled 的值;如果其中有一個 promise 被 rejected,則會馬上 reject。
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "foo");
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
// 預期輸出結果: Array [3, 42, "foo"]
如何實現簡單版的 Promise.all()
在實現完整版之前,我們先以一個僅處理 Array 的 Promise.all
為例子,了解如何實現核心概念,往下再進一步探討如何處理 Iterable
。我們先直接看程式碼,看看你能了解多少。有不懂的地方也不擔心,下面會透過註解,一行行解釋:
function promiseAll(promises) {
if (!Array.isArray(promises)) {
return new TypeError("Arguments must be an array");
}
if (promises.length === 0) {
return Promise.resolve([]);
}
const outputs = [];
let resolveCounter = 0;
return new Promise((resolve, reject) => {
promises.forEach((promise, index) => {
promise()
.then((value) => {
outputs[index] = value;
resolveCounter += 1;
if (resolveCounter === promises.length) {
resolve(outputs);
}
})
.catch(reject);
});
});
}
讓我們透過以下程式碼與註解來看如何實現吧:
function promiseAll(promises) {
// 先檢查輸入是不是 array,如果不是的話就回傳錯誤
if (!Array.isArray(promises)) {
return new TypeError("Arguments must be an array");
}
// 定義中有提到,如果輸入是空的,例如空 array,就 resolve 一個空 array
if (promises.length === 0) {
return Promise.resolve([]);
}
// 先宣告一個最終要 resolve 的 outputs,之後每個 promise 被 fulfilled 時,就放到 outputs 裡面
const outputs = [];
// 我們需要這個 counter 讓我們知道有多少個 promise 已經 fulfilled
let resolveCounter = 0;
// Promise.all() 最終要回傳一個 promise
return new Promise((resolve, reject) => {
promises.forEach((promise, index) => {
promise()
.then((value) => {
// 當輸入的每個 promise 成功 fulfilled 時,就放到 outputs
// 透過 index,我們可以確保順序正確
outputs[index] = value;
// 每次成功放入時,counter 要加一
resolveCounter += 1;
// 當 counter 等於 promises 的長度時,代表所有的 promise 都 fulfilled
// 這時最外面的 promise 就可以 resolve
if (resolveCounter === promises.length) {
resolve(outputs);
}
})
.catch(reject); // 如果有任何一個 reject,就直接 reject
});
});
}
實現完整版的 Promise.all()
上面這個版本的 Promise.all()
只有處理 Array 這種輸入,但實際上的 Promise.all()
是能接收所有的 Iterable
,因此我們可以進一步優化上面的版本 (備註:在面試中能寫出上面的版本,基本上要過關是沒問題,當然如果想在面試中展現自己的細心度,那麼進一步優化是更好的選擇。
先想想,如果要處理任意的 Iterable
可以怎麼做? 我們可以先判斷丟進來的輸入是不是可以迭代的,如果不是的話,就提早回傳錯誤。
另外,實際上的 Promise.all
也能處理非 promise 的 Iterable
,例如處理字串與一般的陣列,所以完整版本我們也會進一步處理這問題
const isIterable =
((typeof promises === "object" && promises !== null) ||
typeof promises === "string") &&
typeof promises[Symbol.iterator] === "function";
if (!isIterable) {
return new TypeError("Arguments must be iterable");
}
基本上多了上述步驟的處理,剩下的邏輯就跟 Array 版本的差不多,程式碼如下 (不同之處會有註解)
function promiseAll(promises) {
// 判斷輸入是否為 Iterable
const isIterable =
((typeof promises === "object" && promises !== null) ||
typeof promises === "string") &&
typeof promises[Symbol.iterator] === "function";
// 不是的話就回傳錯誤訊息
if (!isIterable) {
return new TypeError("Arguments must be iterable");
}
// 把 Iterable 轉成 Array,就可以重複用 Array 版的邏輯
promises = Array.from(promises);
if (promises.length === 0) {
return Promise.resolve([]);
}
const outputs = [];
let resolveCounter = 0;
return new Promise((resolve, reject) => {
// 幫忙處理 resolution 的 helper function
function handleResolution(value, index) {
outputs[index] = value;
resolveCounter += 1;
if (resolveCounter === promises.length) {
resolve(outputs);
}
}
promises.forEach((promise, index) => {
// 這邊要檢查 promise 是不是 thenable
// 例如 如果 promises 是 [1, 2, 3],這邊的 promise 會分別為 1, 2, 3
// 因為不 thenable,我們直接把 1, 2, 3 分別放入 outputs 當中
// 又或者如果 promises 是 "123" 字串,這邊會迭代後,最終輸出 ["1", "2", "3"]
if (promise.then) {
promise()
.then((value) => handleResolution(value, index))
.catch((e) => reject(e));
} else {
handleResolution(promise, index);
}
});
});
}