深入理解 `Promise.all`:从 MDN 规范到手写实现的完整指南

257 阅读6分钟

深入理解 Promise.all:从 MDN 规范到手写实现的完整指南

在现代前端开发中,异步编程是绕不开的话题。而 Promise.all 作为处理多个并发请求的核心工具,不仅是面试常客,更是工程实践中提升性能的关键手段。本文将带你从 MDN 官方定义 出发,深入剖析 Promise.all 的底层原理、可迭代性(Iterable)的本质,并手把手实现一个完整的 Promise.all


一、为什么我们需要 Promise.all

在没有 Promise.all 之前,我们处理多个异步任务时常常陷入“回调地狱”或串行等待的困境:

// ❌ 串行执行:总耗时 = 1s + 2s + 1.5s ≈ 4.5s
async function serialRequests() {
  const res1 = await fetch('/api/user');
  const res2 = await fetch('/api/order');
  const res3 = await fetch('/api/product');
  return [res1, res2, res3];
}

而使用 Promise.all,我们可以让这些请求并行执行

// ✅ 并行执行:总耗时 ≈ 最慢的那个(2s)
async function parallelRequests() {
  const [res1, res2, res3] = await Promise.all([
    fetch('/api/user'),
    fetch('/api/order'),
    fetch('/api/product')
  ]);
  return [res1, res2, res3];
}

🚀 核心优势Promise.all 让多个异步操作同时开始,显著提升了程序响应速度。


二、MDN 官方定义解析

根据 MDN 文档Promise.all() 的定义如下:

Promise.all(iterable) 方法接受一个 可迭代对象(iterable) 作为输入,返回一个新的 Promise 实例。

  • 当输入的所有 Promise 都 fulfilled 时,返回的 Promise 才会变为 fulfilled,其结果是一个包含所有 Promise 返回值的数组,顺序与输入一致。
  • 只要任意一个输入的 Promise 被 rejected,返回的 Promise 就会立即进入 rejected 状态,其返回是第一个被 reject 的值。

🔍 关键点提炼:

特性说明
✅ 输入类型必须是 可迭代对象(iterable)
✅ 返回值一个 Promise 实例
✅ 成功条件所有 Promise 都成功(fulfilled)
✅ 失败条件任一 Promise 失败(rejected),立即失败
✅ 结果顺序保持输入顺序,即使执行完成顺序不同

三、什么是可迭代对象(Iterable)?

Promise.all 接受的参数必须是一个 iterable 类型。那什么是 iterable?

✅ 定义

Iterable(可迭代对象) 是一种实现了 Symbol.iterator 方法的对象,它允许该对象被 for...of、展开运算符 ...Array.from() 等方式遍历。

✅ 常见的 iterable 类型:

类型示例
Array[1, 2, 3]
Mapnew Map([['a', 1], ['b', 2]])
Setnew Set([1, 2, 3])
String'hello'
arguments函数内的 arguments 对象
NodeListDOM 查询结果(如 document.querySelectorAll('div')

✅ 判断是否为 iterable

function isIterable(obj) {
  return obj != null && typeof obj[Symbol.iterator] === 'function';
}

console.log(isIterable([1, 2, 3])); // true
console.log(isIterable('hello'));   // true
console.log(isIterable({}));        // false

✅ 在 Promise.all 中的应用

Promise.all([p1, p2, p3])           // ✅ Array
Promise.all(new Set([p1, p2, p3]))  // ✅ Set
Promise.all(new Map([[1, p1], [2, p2]]).values()) // ✅ Map.values() 返回 iterable

四、Promise.all 的并行执行原理

🌐 并行 ≠ 同时完成

很多人误以为“并行”意味着所有任务在同一时刻完成。实际上:

  • 并行(Parallel):多个任务同时开始执行
  • 完成时间:取决于最慢的那个任务
const p1 = Promise.resolve('p1'); // 立即完成
const p2 = new Promise(r => setTimeout(() => r('p2'), 1000)); // 1s 后完成
const p3 = new Promise(r => setTimeout(() => r('p3'), 2000)); // 2s 后完成

console.time('Promise.all');
Promise.all([p1, p2, p3]).then(console.log);
// 输出: ['p1', 'p2', 'p3']
// 耗时: ~2s
console.timeEnd('Promise.all');

✅ 所有 Promise 在调用 Promise.all 时就已经开始执行,互不等待。

Promise.all 的并行性能最终取决于宿主环境能否真正并发执行 I/O 操作

操作类型底层实现是否真正并行
fetch / XMLHttpRequest浏览器网络栈(多线程)✅ 是
setTimeout / setInterval浏览器定时器线程✅ 是
fs.readFile (Node.js)libuv 线程池✅ 是
CPU 密集型计算JavaScript 主线程❌ 否(会阻塞)

⚠️ 如果你在 Promise 中执行大量同步计算:

new Promise(r => {
  let sum = 0;
  for (let i = 0; i < 1e9; i++) sum += i; // 阻塞主线程
  r(sum);
})

这种“Promise”并不会并行,因为它占用了主线程,导致其他任务无法执行。

五、关键问题:即使有 Promise 被 rejected,其他任务还会继续执行吗?

✅ 答案:会!

这是一个非常重要的知识点:Promise.all 不会取消或中断其他正在运行的 Promise

🧪 实验验证

const p1 = Promise.resolve('p1');
const p2 = new Promise((_, reject) => 
  setTimeout(() => reject('p2 failed'), 1500)
);
const p3 = new Promise(resolve => 
  setTimeout(() => resolve('p3 completed'), 2000)
);

console.log('Start:', new Date().toISOString());

Promise.all([p1, p2, p3])
  .then(console.log)
  .catch(err => {
    console.log('Catch:', err, new Date().toISOString());
    // 但 p3 仍在运行,2s 后仍会输出 'p3 completed'
  });

// 输出:
// Start: 2025-08-30T08:00:00.000Z
// Catch: p2 failed 2025-08-30T08:00:01.500Z
// (1.5s 后)p3 completed (2s 后)

❓ 为什么设计成这样?

  1. 职责分离Promise.all 只负责监听状态,不负责控制执行
  2. 资源不可逆:网络请求一旦发出,无法“收回”
  3. 副作用可能已发生:比如上传文件、写数据库等操作不可撤销
  4. 符合 Promise 设计哲学:Promise 是对“未来值”的抽象,不应被外部轻易取消

⚠️ 如果你需要中断未完成的任务,应使用 AbortControllerPromise.race 等机制。


六、Promise.all 与其他并行方法对比

方法行为适用场景
Promise.all全成功才成功,任一失败即失败所有数据必须同时获取(如表单验证)
Promise.race谁快听谁的(首个完成即返回)超时控制、竞速请求
Promise.any首个成功即成功,全失败才失败多源备份请求(如 CDN 切换)
Promise.allSettled等待全部完成,无论成败批量操作需统计结果(如批量上传)

七、手写 Promise.all:从零实现一个工业级版本

现在我们来手写一个完整的 Promise.all,命名为 Promise.myAll

✅ 核心逻辑

  1. 返回一个新的 Promise
  2. 遍历 iterable,确保每一项都是 Promise
  3. 收集结果,保持顺序
  4. 任一失败立即 reject
  5. 全部成功后 resolve 结果数组

✅ 实现代码

Promise.myAll = function (iterable) {
  // 1. 检查输入是否为可迭代对象
  if (iterable == null || typeof iterable[Symbol.iterator] !== 'function') {
    return Promise.reject(new TypeError('Argument is not iterable'));
  }

  // 2. 转换为数组,避免重复遍历
  const promises = Array.from(iterable);

  // 3. 空数组直接返回 resolve([])
  if (promises.length === 0) {
    return Promise.resolve([]);
  }

  return new Promise((resolve, reject) => {
    const results = new Array(promises.length); // 预分配数组,保持顺序
    let completedCount = 0;

    promises.forEach((promise, index) => {
      // 4. 使用 Promise.resolve 包装,确保是 Promise
      Promise.resolve(promise)
        .then(value => {
          results[index] = value;
          completedCount++;

          // 5. 全部完成则 resolve
          if (completedCount === promises.length) {
            resolve(results);
          }
        })
        .catch(reason => {
          // 6. 任一失败立即 reject(短路)
          reject(reason);
        });
    });
  });
};

✅ 测试用例

// 测试正常情况
Promise.myAll([Promise.resolve(1), Promise.resolve(2)])
  .then(console.log) // [1, 2]

// 测试失败情况
Promise.myAll([Promise.resolve(1), Promise.reject('error')])
  .catch(console.log) // 'error'

// 测试非 Promise 输入
Promise.myAll([1, 2, 3])
  .then(console.log) // [1, 2, 3]

// 测试空数组
Promise.myAll([])
  .then(console.log) // []

八、常见错误与最佳实践

❌ 错误用法

// 1. 忘记 await
const result = Promise.all([p1, p2]); // 返回 Promise,不是数组

// 2. 使用 for...in 遍历数组(错误)
for (let i in promises) { ... }

// 3. 不处理 reject
Promise.all([p1, p2]).then(...); // 没有 catch,错误会抛出

✅ 最佳实践

// 1. 总是使用 try/catch 或 .catch()
try {
  const results = await Promise.all([p1, p2]);
} catch (err) {
  console.error('请求失败:', err);
}

// 2. 处理部分失败(使用 allSettled)
const results = await Promise.allSettled([p1, p2, p3]);
const successes = results.filter(r => r.status === 'fulfilled');
const failures = results.filter(r => r.status === 'rejected');

九、总结:Promise.all 的核心要点

要点说明
✅ 并行执行所有 Promise 同时开始
✅ 保持顺序结果数组顺序与输入一致
✅ 短路失败任一失败立即 reject
✅ 不中断其他已发起的 Promise 不会停止
✅ 输入为 iterable支持数组、Set、Map 等
✅ 返回新 Promise符合链式调用规范

十、结语

Promise.all 不仅仅是一个语法糖,它是现代异步编程的基石之一。掌握它的:

  • MDN 规范
  • Iterable 概念
  • 并行原理
  • 失败处理策略
  • 手写实现能力

不仅能帮你写出更高效的代码,更能让你在面试中从容应对“手写 Promise.all”这类经典问题。

下次当你看到 Promise.all,不要只把它当作一个函数,而要看到背后并发控制、错误处理、设计哲学的完整体系。