Promise.all 是什么?请实现 Promise.all

2022年10月6日

💎 加入 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 上追蹤我們