Promise.all 与 Promise.allSettled 手写实现
在 JavaScript 异步编程中,Promise.all 和 Promise.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. 计数器追踪完成数量
typescriptlet 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 的设计
typescriptreturn 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.all | Promise.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.all和Promise.allSettled是处理并发异步操作的两种核心模式。前者适用于"全部成功才算成功"的场景,后者适用于"需要知道所有结果"的场景。
理解这两个方法的内部实现,关键在于掌握三点:
- 计数器追踪完成数量 - 使用独立计数器而非数组长度
- 按索引存储保证顺序 - 使用
results[index]而非push - 统一包装非 Promise 值 - 使用
Promise.resolve(item)处理
在实际项目中,根据业务需求选择合适的方法,并配合并发控制、超时处理等策略,能让你的异步代码更加健壮和优雅。记住:在异步编程中,"控制"比"速度"更重要。



