Promise.all 是什麼?請實現 Promise.all

2023年12月3日

💎 加入 E+ 成長計畫 與超過 300+ 位軟體工程師一同在社群中成長,並且獲得更多的軟體工程學習資源

作為前端工程師,在日常的開發中很常用到 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);
      }
    });
  });
}
🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們