实战指南

Promise.all 与 Promise.allSettled 手写实现:深入理解并发控制的核心机制

深入探讨 Promise.all 和 Promise.allSettled 的实现原理,掌握 JavaScript 异步并发控制的核心技术

2026年1月25日12 分钟1103 阅读
Promise异步编程并发控制JavaScript
Promise.all 与 Promise.allSettled 手写实现:深入理解并发控制的核心机制

Promise.all 与 Promise.allSettled 手写实现

在 JavaScript 异步编程中,Promise.allPromise.allSettled 是处理多个并发 Promise 的核心方法。理解它们的实现原理,不仅能帮助我们更好地使用这些 API,还能提升对异步编程模式的整体认知。

Promise.all 实现解析

Promise.all 接收一个 Promise 数组,当所有 Promise 都成功时返回结果数组,任意一个失败则立即拒绝。这种"快速失败"的策略适用于"全部成功才算成功"的场景。

核心实现

typescript
/**
 * 手写 Promise.all
 * @param {Array} promises - 包含 Promise 或非 Promise 值的数组
 * @returns {Promise} - 全部成功则返回结果数组,任意一个失败则立即拒绝
 */
function promiseAll(promises) {
  // 返回一个新的 Promise
  return new Promise((resolve, reject) => {
    // 1. 校验参数必须是数组
    if (!Array.isArray(promises)) {
      return reject(new TypeError("参数必须是一个数组"));
    }

    // 2. 处理空数组的情况:直接 resolve 空数组
    const len = promises.length;
    if (len === 0) {
      return resolve([]);
    }

    const results = []; // 存储所有成功结果
    let resolvedCount = 0; // 已完成的 Promise 数量

    // 3. 遍历每个 Promise
    promises.forEach((item, index) => {
      // 确保每个项都是 Promise(非 Promise 则包装成 Promise)
      Promise.resolve(item).then(
        (value) => {
          // 按原数组顺序存储结果
          results[index] = value;
          resolvedCount++;

          // 所有 Promise 都成功时,resolve 结果数组
          if (resolvedCount === len) {
            resolve(results);
          }
        },
        (reason) => {
          // 任意一个 Promise 失败,立即 reject
          reject(reason);
        },
      );
    });
  });
}

// 使用示例
promiseAll([Promise.resolve(1), Promise.resolve(2), 3])
  .then(console.log);  // [1, 2, 3]

promiseAll([Promise.resolve(1), Promise.reject('error')])
  .catch(console.log); // 'error'

promiseAll([])
  .then(console.log);  // []

关键设计点

特性说明
参数校验必须传入数组,否则 reject TypeError
空数组处理直接 resolve 空数组 []
结果顺序使用 results[index] 保证结果顺序与输入一致
非 Promise 处理通过 Promise.resolve(item) 统一包装
失败策略快速失败(fail-fast),任一失败立即 reject

实现要点详解

1. 计数器追踪完成数量

typescript
let resolvedCount = 0;

// 每个 Promise 成功后计数
resolvedCount++;

// 当计数等于总数时,所有 Promise 都已完成
if (resolvedCount === len) {
  resolve(results);
}

为什么不用 results.length === len?因为数组是稀疏的,直接赋值 results[index] 不会改变 length

2. 按索引存储保证顺序

typescript
// 使用 index 而非 push,确保结果顺序与输入一致
results[index] = value;

Promise 的完成顺序是不确定的,但用户期望结果顺序与输入一致。

3. 快速失败机制

typescript
(reason) => {
  // 第一个失败的 Promise 会触发整体 reject
  reject(reason);
}

一旦有 Promise 失败,立即 reject,不等待其他 Promise。

Promise.allSettled 实现解析

Promise.allSettled 等待所有 Promise 完成(无论成功或失败),返回每个 Promise 的状态和结果。这种"全部等待"的策略适用于"需要知道所有结果"的场景。

核心实现

typescript
/**
 * 手写 Promise.allSettled
 * @param {Array} promises - 包含 Promise 或非 Promise 值的数组
 * @returns {Promise} - 始终 resolve,结果数组包含每个 Promise 的完成状态
 */
function promiseAllSettled(promises) {
  return new Promise((resolve) => {
    // 1. 校验参数必须是数组
    if (!Array.isArray(promises)) {
      return resolve([
        {
          status: "rejected",
          reason: new TypeError("参数必须是一个数组"),
        },
      ]);
    }

    const len = promises.length;
    // 2. 处理空数组
    if (len === 0) {
      return resolve([]);
    }

    const results = [];
    let settledCount = 0; // 已完成(成功/失败)的 Promise 数量

    // 3. 遍历每个 Promise
    promises.forEach((item, index) => {
      Promise.resolve(item)
        .then(
          (value) => {
            // 成功的结果格式
            results[index] = { status: "fulfilled", value };
          },
          (reason) => {
            // 失败的结果格式
            results[index] = { status: "rejected", reason };
          },
        )
        .finally(() => {
          // 无论成功/失败,都计数+1
          settledCount++;
          // 所有 Promise 都完成后,resolve 结果数组
          if (settledCount === len) {
            resolve(results);
          }
        });
    });
  });
}

// 使用示例
promiseAllSettled([
  Promise.resolve(1),
  Promise.reject('error'),
  3
]).then(console.log);
// [
//   { status: "fulfilled", value: 1 },
//   { status: "rejected", reason: "error" },
//   { status: "fulfilled", value: 3 }
// ]

关键设计点

特性说明
永不 reject始终返回 resolve,即使所有 Promise 都失败
状态标识成功为 fulfilled,失败为 rejected
结果格式成功:{ status, value },失败:{ status, reason }
完成计数使用 finally 统一计数,确保成功/失败都被统计

实现要点详解

1. 统一的结果格式

typescript
// 成功时
results[index] = { status: "fulfilled", value };

// 失败时
results[index] = { status: "rejected", reason };

标准化的结果格式让调用者可以统一处理。

2. 使用 finally 统一计数

typescript
.finally(() => {
  settledCount++;
  if (settledCount === len) {
    resolve(results);
  }
});

finally 无论 Promise 成功或失败都会执行,避免重复代码。

3. 永不 reject 的设计

typescript
return new Promise((resolve) => {
  // 注意:没有 reject 参数
  // ...
});

Promise.allSettled 的设计哲学是"报告所有结果",而非"报告第一个错误"。

两者对比

typescript
// Promise.all: 快速失败策略
promiseAll([
  fetch('/api/user'),
  fetch('/api/posts'),
  fetch('/api/comments')
])
// 适用场景:所有请求都必须成功,任一失败则整体失败

// Promise.allSettled: 全部等待策略  
promiseAllSettled([
  fetch('/api/user'),
  fetch('/api/posts'),
  fetch('/api/comments')
])
// 适用场景:需要知道每个请求的结果,即使部分失败也要处理成功的

行为差异总结

特性Promise.allPromise.allSettled
失败策略快速失败,立即 reject等待全部完成
返回值成功:结果数组;失败:第一个错误始终返回状态对象数组
适用场景全部成功才有意义需要知道所有结果
错误处理需要 catch在结果中检查 status

最佳实践

1. 选择合适的方法

typescript
// 错误:用 Promise.all 处理部分失败也需要继续的场景
async function loadDashboard() {
  const [user, posts] = await Promise.all([
    fetchUser(),     // 如果失败,整个 dashboard 无法加载
    fetchPosts()
  ])
}

// 正确:用 Promise.allSettled 优雅降级
async function loadDashboard() {
  const results = await Promise.allSettled([
    fetchUser(),
    fetchPosts()
  ])
  
  const user = results[0].status === 'fulfilled' ? results[0].value : null
  const posts = results[1].status === 'fulfilled' ? results[1].value : []
}

2. 封装结果处理工具

typescript
/**
 * 从 allSettled 结果中提取成功的值
 */
function getFulfilledValues<T>(
  results: PromiseSettledResult<T>[]
): T[] {
  return results
    .filter((r): r is PromiseFulfilledResult<T> => r.status === 'fulfilled')
    .map(r => r.value)
}

/**
 * 从 allSettled 结果中提取失败的原因
 */
function getRejectedReasons(
  results: PromiseSettledResult<unknown>[]
): unknown[] {
  return results
    .filter((r): r is PromiseRejectedResult => r.status === 'rejected')
    .map(r => r.reason)
}

// 使用示例
const results = await Promise.allSettled([
  fetch('/api/1'),
  fetch('/api/2'),
  fetch('/api/3')
])

const successData = getFulfilledValues(results)
const errors = getRejectedReasons(results)

3. 并发控制

typescript
/**
 * 带并发限制的 Promise.all
 */
async function promiseAllWithLimit<T>(
  tasks: (() => Promise<T>)[],
  limit: number
): Promise<T[]> {
  const results: T[] = []
  const executing: Promise<void>[] = []

  for (const [index, task] of tasks.entries()) {
    const p = task().then(result => {
      results[index] = result
    })

    executing.push(p)

    if (executing.length >= limit) {
      await Promise.race(executing)
      executing.splice(
        executing.findIndex(e => e === p),
        1
      )
    }
  }

  await Promise.all(executing)
  return results
}

// 使用示例:最多同时 5 个并发请求
const urls = [/* 100 个 URL */]
const results = await promiseAllWithLimit(
  urls.map(url => () => fetch(url)),
  5
)

4. 超时控制

typescript
/**
 * 带超时的 Promise.all
 */
function promiseAllWithTimeout<T>(
  promises: Promise<T>[],
  timeout: number
): Promise<T[]> {
  const timeoutPromise = new Promise<never>((_, reject) => {
    setTimeout(() => reject(new Error('Timeout')), timeout)
  })

  return Promise.race([
    Promise.all(promises),
    timeoutPromise
  ])
}

// 使用示例:5 秒超时
const results = await promiseAllWithTimeout([
  fetch('/api/1'),
  fetch('/api/2')
], 5000)

常见面试题

1. 为什么用计数器而不是 results.length?

typescript
// 错误做法
if (results.length === len) {
  resolve(results);
}

// 问题:数组是稀疏的
const arr = [];
arr[2] = 'value';
console.log(arr.length); // 3,但实际只有 1 个元素

2. 如何保证结果顺序?

typescript
// 使用 index 定位,而非 push
results[index] = value;

// Promise 完成顺序:2, 0, 1
// 结果数组顺序:[result0, result1, result2]

3. Promise.resolve 的作用?

typescript
// 统一处理 Promise 和非 Promise 值
Promise.resolve(item).then(...)

// 等价于
if (item instanceof Promise) {
  item.then(...)
} else {
  Promise.resolve(item).then(...)
}

总结

"

Promise.allPromise.allSettled 是处理并发异步操作的两种核心模式。前者适用于"全部成功才算成功"的场景,后者适用于"需要知道所有结果"的场景。

理解这两个方法的内部实现,关键在于掌握三点:

  1. 计数器追踪完成数量 - 使用独立计数器而非数组长度
  2. 按索引存储保证顺序 - 使用 results[index] 而非 push
  3. 统一包装非 Promise 值 - 使用 Promise.resolve(item) 处理

在实际项目中,根据业务需求选择合适的方法,并配合并发控制、超时处理等策略,能让你的异步代码更加健壮和优雅。记住:在异步编程中,"控制"比"速度"更重要。

文章标签

# Promise# 异步编程# 并发控制# JavaScript
返回首页