Published on

Promise.all:从规范到手写实现

Promise.all 的规范与要求

根据 ECMAScript 规范,一个健壮的 Promise.all 实现必须满足以下核心要求:

Promise.all 的核心规范

  1. 返回 Promise: Promise.all 函数必须返回一个新的 Promise 实例。
  2. 接受可迭代对象: 参数必须是一个可迭代对象 (iterable)(如 Array, Map, Set, 字符串等),其成员可以是 Promise 或非 Promise 值。
  3. 参数校验: 如果传入的参数不是一个可迭代对象,必须立即返回一个 rejected 状态的 Promise,并附带一个 TypeError
  4. 空迭代器处理: 如果传入的可迭代对象为空,必须立即返回一个 fulfilled 状态的 Promise,其值为一个空数组 []
  5. 成功条件: 只有当所有输入的 Promise 都变为 fulfilled 时,返回的 Promise 才会 fulfilled。其成功的值是一个数组,包含了所有输入 Promise 的结果,且顺序与输入时完全一致
  6. 失败条件: 只要有任意一个输入的 Promise 变为 rejected,返回的 Promise 就会立即 rejected,其失败的原因就是第一个失败的 Promise 的原因。

Promise.all 的 Polyfill 实现

基于上述规范,我们可以构建一个 Promise.all 的 polyfill。这个过程的核心在于如何正确地追踪所有 Promise 的状态、维持结果的顺序以及处理“快速失败”的逻辑。

健壮的 Promise.all 实现

function promiseAll(iterable) {
  return new Promise((resolve, reject) => {
    // 1. 验证参数是否为可迭代对象
    if (iterable == null || typeof iterable[Symbol.iterator] !== 'function') {
      return reject(new TypeError('Argument is not iterable.'));
    }

    const items = Array.from(iterable);
    const itemCount = items.length;

    // 2. 处理空迭代器的边界情况
    if (itemCount === 0) {
      return resolve([]);
    }

    const results = new Array(itemCount);
    let pendingCount = itemCount;
    let hasRejected = false;

    // 3. 遍历所有项目
    items.forEach((item, index) => {
      // 使用 Promise.resolve 包装,以统一处理 Promise 和非 Promise 值
      Promise.resolve(item).then(
        // onFulfilled 回调
        (value) => {
          // 如果已有其他 Promise 失败,则忽略此成功结果
          if (hasRejected) return;

          // 将结果按原始顺序存放在结果数组的对应位置
          results[index] = value;
          pendingCount--;

          // 如果所有 Promise 都已成功,则 resolve 最终的 Promise
          if (pendingCount === 0) {
            resolve(results);
          }
        },
        // onRejected 回调
        (reason) => {
          // 确保只 reject 一次(“快速失败”机制)
          if (hasRejected) return;
          hasRejected = true;
          
          // 立即 reject 最终的 Promise
          reject(reason);
        }
      );
    });
  });
}

关键行为深度解析

结果的顺序保证

Promise.all 的一个核心特性是,无论内部的 Promise 以何种顺序完成,最终成功时返回的结果数组,其顺序都与传入的可迭代对象的顺序严格一致。

实现原理

在实现中,我们首先根据输入项的数量,创建一个固定长度的、空的 results 数组。当每个 Promise 完成时,我们是根据它在原始数组中的索引 (index),将结果直接放入 results 数组的对应位置,而不是 push 进去。这确保了最终结果的顺序性。

“快速失败 (Fail-Fast)” 与不可中止的异步操作

Promise.all 不会中止其他操作

Promise.all 因为一个 Promise 失败而立即 reject 时,它只是停止了对其他 Promise 结果的等待

它没有能力,也不会去中止那些已经在后台运行的其他异步操作(例如,其他未完成的 fetch 请求)。这些请求会继续在后台运行,直到它们完成或失败。只是它们最终的结果,会被 Promise.all 完全忽略。在需要进行资源清理的场景下(如中止 fetch),应在每个独立的 Promise 中结合 AbortController 来实现。