手写 Promise 静态方法
问题
手写实现 Promise.all、Promise.race、Promise.allSettled、Promise.any 四个静态方法。
答案
Promise 的四个静态方法是前端面试中出现频率极高的手写题。它们都接收一个 可迭代对象(Iterable),但在处理成功、失败的策略上各不相同。理解它们的差异和实现细节,不仅有助于面试,也能让你在实际开发中选择最合适的并发控制方案。
在阅读本文之前,建议先了解 手写 Promise 中 Promise 的核心实现(状态管理、链式调用),以及 异步编程 中 Promise 的基本用法。
四个方法对比总览
| 方法 | 全部成功 | 部分失败 | 全部失败 | 返回值 | ES 版本 |
|---|---|---|---|---|---|
Promise.all | resolve 结果数组 | 第一个 reject | 第一个 reject | T[] / 第一个错误 | ES2015 |
Promise.race | 第一个 settle 的值 | 第一个 settle 的值 | 第一个 settle 的值 | 第一个结果 | ES2015 |
Promise.allSettled | settled 状态数组 | settled 状态数组 | settled 状态数组 | PromiseSettledResult<T>[] | ES2020 |
Promise.any | 第一个 resolve 的值 | 第一个 resolve 的值 | AggregateError | 第一个成功值 | ES2021 |
一、Promise.all
Promise.all 接收一个可迭代对象,当所有 Promise 都成功时返回结果数组,任何一个失败则立即 reject(快速失败策略)。
function promiseAll<T>(promises: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>[]> {
return new Promise((resolve, reject) => {
const results: Awaited<T>[] = [];
let count = 0; // 总数
let resolvedCount = 0; // 已完成数
for (const promise of promises) {
const index = count++; // 用闭包捕获当前索引,保证结果顺序
Promise.resolve(promise).then(
(value) => {
results[index] = value; // 按索引赋值,不用 push,保持顺序
if (++resolvedCount === count) {
resolve(results); // 全部完成才 resolve
}
},
(reason) => reject(reason) // 快速失败:第一个 reject 立即结束
);
}
if (count === 0) resolve(results); // 空可迭代对象直接 resolve 空数组
});
}
- 保持顺序:用
index记录每个 Promise 的位置,而不是push。虽然 Promise 完成的时间不确定,但结果数组的顺序必须与输入一致。 - 空数组处理:
Promise.all([])会同步 resolve 一个空数组[]。 - 非 Promise 值:通过
Promise.resolve(promise)将普通值包装为 Promise,确保统一处理。 - 快速失败:只要有一个 reject,整个
Promise.all就立即 reject,其他 Promise 的结果会被忽略(但不会被取消,它们仍会继续执行)。
使用示例:
// 全部成功
const results = await promiseAll([
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3),
]);
console.log(results); // [1, 2, 3]
// 部分失败 → 快速失败
try {
await promiseAll([
Promise.resolve(1),
Promise.reject(new Error('失败')),
Promise.resolve(3), // 这个结果会被忽略
]);
} catch (error) {
console.error(error.message); // '失败'
}
// 空数组
const empty = await promiseAll([]);
console.log(empty); // []
二、Promise.race
Promise.race 返回第一个 settle(无论 fulfilled 还是 rejected)的 Promise 结果。
function promiseRace<T>(promises: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>> {
return new Promise((resolve, reject) => {
for (const promise of promises) {
Promise.resolve(promise).then(resolve, reject); // 第一个 settle 的决定结果
}
// 注意:空 Iterable 不会调用 resolve 或 reject
// 空 Iterable → 返回的 Promise 永远 pending
});
}
Promise 的状态一旦改变就不可逆转。第一个 settle 的 Promise 调用 resolve 或 reject 后,后续的调用会被静默忽略。这是 Promise 规范保证的行为,不需要额外加锁或判断。
Promise.race([]) 返回的 Promise 永远处于 pending 状态,永远不会 resolve 或 reject。这可能导致内存泄漏或逻辑死锁,使用时务必确保传入的数组非空。
使用示例:
// 竞速:返回最快的
const result = await promiseRace([
new Promise<string>(resolve => setTimeout(() => resolve('慢'), 200)),
new Promise<string>(resolve => setTimeout(() => resolve('快'), 100)),
]);
console.log(result); // '快'
// 第一个是 reject → 整体 reject
try {
await promiseRace([
new Promise((_, reject) => setTimeout(() => reject(new Error('先失败')), 50)),
new Promise(resolve => setTimeout(() => resolve('后成功'), 100)),
]);
} catch (error) {
console.error(error.message); // '先失败'
}
三、Promise.allSettled
Promise.allSettled 等待所有 Promise 都 settle(无论成功还是失败),返回一个包含每个 Promise 结果状态的数组。它永远 resolve,不会 reject。
function promiseAllSettled<T>(
promises: Iterable<T | PromiseLike<T>>
): Promise<PromiseSettledResult<Awaited<T>>[]> {
return new Promise((resolve) => {
const results: PromiseSettledResult<Awaited<T>>[] = [];
let count = 0;
let settledCount = 0;
for (const promise of promises) {
const index = count++;
Promise.resolve(promise).then(
(value) => {
results[index] = { status: 'fulfilled', value };
if (++settledCount === count) resolve(results);
},
(reason) => {
results[index] = { status: 'rejected', reason };
if (++settledCount === count) resolve(results);
}
);
}
if (count === 0) resolve(results); // 空数组直接 resolve
});
}
- 永远 resolve:无论内部 Promise 成功还是失败,
allSettled本身永远以 fulfilled 状态结束。 - 结果格式统一:每个元素都是
{ status: 'fulfilled', value }或{ status: 'rejected', reason },通过status字段区分。 - 适合批量操作:当你需要知道每个操作的结果(而不是遇到一个失败就放弃)时,使用
allSettled。
使用示例:
const results = await promiseAllSettled([
Promise.resolve('成功1'),
Promise.reject(new Error('失败')),
Promise.resolve('成功2'),
]);
console.log(results);
// [
// { status: 'fulfilled', value: '成功1' },
// { status: 'rejected', reason: Error('失败') },
// { status: 'fulfilled', value: '成功2' }
// ]
// 筛选成功和失败的结果
const fulfilled = results.filter(
(r): r is PromiseFulfilledResult<string> => r.status === 'fulfilled'
);
const rejected = results.filter(
(r): r is PromiseRejectedResult => r.status === 'rejected'
);
四、Promise.any
Promise.any 返回第一个 成功(fulfilled)的 Promise 结果。只有当所有 Promise 都失败时,才会 reject 一个 AggregateError。
function promiseAny<T>(promises: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>> {
return new Promise((resolve, reject) => {
const errors: any[] = [];
let count = 0;
let rejectedCount = 0;
for (const promise of promises) {
const index = count++;
Promise.resolve(promise).then(
(value) => resolve(value), // 第一个成功即 resolve
(reason) => {
errors[index] = reason; // 按顺序记录错误
if (++rejectedCount === count) {
// 全部失败才 reject
reject(new AggregateError(errors, 'All promises were rejected'));
}
}
);
}
if (count === 0) {
reject(new AggregateError([], 'All promises were rejected'));
}
});
}
- 与
Promise.race的区别:race返回第一个 settle 的结果(不管成功失败),any会忽略 rejection,只关心第一个成功的。 - AggregateError:ES2021 新增的错误类型,继承自
Error,有一个errors属性是所有错误的数组。 - 空 Iterable:
Promise.any([])会直接 reject 一个 AggregateError,与race的永远 pending 不同。
使用示例:
// 返回第一个成功的
const result = await promiseAny([
Promise.reject('错误1'),
new Promise<string>(resolve => setTimeout(() => resolve('成功'), 100)),
Promise.reject('错误2'),
]);
console.log(result); // '成功'
// 全部失败 → AggregateError
try {
await promiseAny([
Promise.reject('错误1'),
Promise.reject('错误2'),
Promise.reject('错误3'),
]);
} catch (error) {
if (error instanceof AggregateError) {
console.log(error.message); // 'All promises were rejected'
console.log(error.errors); // ['错误1', '错误2', '错误3']
}
}
五、四个方法对空数组的处理
这是面试中非常高频的考点:
| 方法 | Promise.xxx([]) 的行为 | 原因 |
|---|---|---|
Promise.all([]) | 立即 resolve [] | 「全部成功」的空集为真 |
Promise.race([]) | 永远 pending | 没有任何 Promise 可以 settle |
Promise.allSettled([]) | 立即 resolve [] | 「全部已完成」的空集为真 |
Promise.any([]) | 立即 reject AggregateError | 没有任何成功的 Promise |
Promise.race([]) 永远不会 settle,如果你在 await 它,代码会永远挂起。在实际开发中,务必在调用 Promise.race 前检查数组是否为空。
六、实际应用场景
- Promise.all
- Promise.race
- Promise.allSettled
- Promise.any
场景:并行请求,全部成功才继续
interface PageData {
user: User;
orders: Order[];
settings: Settings;
}
async function loadPageData(userId: string): Promise<PageData> {
const [user, orders, settings] = await Promise.all([
fetchUser(userId),
fetchOrders(userId),
fetchSettings(userId),
]);
return { user, orders, settings };
}
场景:超时控制、竞速请求
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
// 使用
const data = await withTimeout(fetch('/api/data'), 5000);
场景:批量操作,允许部分失败
async function batchDelete(ids: string[]): Promise<{ success: number; failed: number }> {
const results = await Promise.allSettled(
ids.map(id => deleteItem(id))
);
const success = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
return { success, failed };
}
场景:多源竞速,取最快成功的
async function fetchFromFastestCDN(path: string): Promise<Response> {
const cdnList = [
'https://cdn1.example.com',
'https://cdn2.example.com',
'https://cdn3.example.com',
];
return Promise.any(
cdnList.map(cdn => fetch(`${cdn}${path}`))
);
}
七、进阶实现
1. 带并发限制的 Promise.all
标准 Promise.all 会同时启动所有 Promise,对于大量请求可能造成服务端压力。结合 限流调度器 的思路,实现一个带并发限制的版本:
async function promiseAllWithLimit<T>(
tasks: Array<() => Promise<T>>,
limit: number
): Promise<T[]> {
const results: T[] = [];
const executing: Set<Promise<void>> = new Set();
for (const [index, task] of tasks.entries()) {
const p = task().then((result) => {
results[index] = result;
});
const clean: Promise<void> = p.then(() => {
executing.delete(clean);
});
executing.add(clean);
if (executing.size >= limit) {
await Promise.race(executing); // 等最快完成的一个
}
}
await Promise.all(executing); // 等待剩余任务
return results;
}
// 使用:最多同时 3 个请求
const urls = Array.from({ length: 20 }, (_, i) => `/api/item/${i}`);
const results = await promiseAllWithLimit(
urls.map(url => () => fetch(url).then(r => r.json())),
3
);
更完整的并发控制方案请参考 限流调度器 和 异步串行与并行。
2. 带超时的 Promise.race
function promiseRaceWithTimeout<T>(
promises: Array<Promise<T>>,
timeout: number,
timeoutMessage: string = 'Operation timed out'
): Promise<T> {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error(timeoutMessage)), timeout);
});
return Promise.race([...promises, timeoutPromise]);
}
// 使用
try {
const result = await promiseRaceWithTimeout(
[fetch('/api/slow'), fetch('/api/fast')],
3000,
'请求超时'
);
} catch (error) {
console.error(error.message); // 如果 3 秒内没有任何请求完成,输出 '请求超时'
}
3. 可取消的 Promise.all
interface CancellableResult<T> {
promise: Promise<T[]>;
cancel: (reason?: string) => void;
}
function cancellablePromiseAll<T>(
tasks: Array<() => Promise<T>>
): CancellableResult<T> {
const controller = new AbortController();
const promise = new Promise<T[]>((resolve, reject) => {
const results: T[] = [];
let count = 0;
let resolvedCount = 0;
controller.signal.addEventListener('abort', () => {
reject(new DOMException(
controller.signal.reason || 'Cancelled',
'AbortError'
));
});
for (const task of tasks) {
const index = count++;
if (controller.signal.aborted) return;
task().then(
(value) => {
if (controller.signal.aborted) return;
results[index] = value;
if (++resolvedCount === count) resolve(results);
},
(reason) => {
if (controller.signal.aborted) return;
reject(reason);
}
);
}
if (count === 0) resolve(results);
});
return {
promise,
cancel: (reason?: string) => controller.abort(reason),
};
}
// 使用
const { promise, cancel } = cancellablePromiseAll([
() => fetch('/api/1').then(r => r.json()),
() => fetch('/api/2').then(r => r.json()),
]);
// 需要取消时
cancel('用户离开页面');
常见面试问题
Q1: 手写 Promise.all
答案:
核心要点:
- 接收
Iterable,用for...of遍历 - 用
index保证结果顺序(闭包捕获索引) - 用计数器判断全部完成
- 任一 reject 立即 reject(快速失败)
- 空数组直接 resolve
[] Promise.resolve()包装非 Promise 值
完整实现见上方 Promise.all 部分。面试时可以先写出骨架,再逐步补充边界处理:
function promiseAll<T>(promises: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>[]> {
return new Promise((resolve, reject) => {
const results: Awaited<T>[] = [];
let count = 0;
let resolved = 0;
for (const p of promises) {
const i = count++;
Promise.resolve(p).then(
val => { results[i] = val; if (++resolved === count) resolve(results); },
reject
);
}
if (count === 0) resolve(results);
});
}
Q2: 手写 Promise.race
答案:
Promise.race 是四个方法中实现最简单的:
function promiseRace<T>(promises: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>> {
return new Promise((resolve, reject) => {
for (const p of promises) {
Promise.resolve(p).then(resolve, reject);
}
});
}
关键考点:
- 为什么多次调用
resolve/reject没问题?因为 Promise 状态不可逆,第一次调用后后续调用自动忽略。 - 空 Iterable 会怎样?返回的 Promise 永远 pending,这是一个需要注意的边界情况。
Q3: Promise.all 和 Promise.allSettled 的区别?
答案:
| 对比项 | Promise.all | Promise.allSettled |
|---|---|---|
| 失败处理 | 任一失败立即 reject(快速失败) | 等待全部完成,永远 resolve |
| 返回值 | 成功值数组 T[] | 状态对象数组 PromiseSettledResult<T>[] |
| 适用场景 | 全部必须成功(页面加载) | 允许部分失败(批量操作) |
| ES 版本 | ES2015 | ES2020 |
// Promise.all:全部成功才渲染页面
try {
const [user, config] = await Promise.all([fetchUser(), fetchConfig()]);
renderPage(user, config);
} catch (error) {
showErrorPage(); // 任一失败就展示错误页
}
// Promise.allSettled:批量删除,部分失败也要继续
const results = await Promise.allSettled(ids.map(id => deleteItem(id)));
const failedIds = results
.map((r, i) => r.status === 'rejected' ? ids[i] : null)
.filter(Boolean);
if (failedIds.length > 0) {
showToast(`${failedIds.length} 项删除失败,请重试`);
}
Q4: Promise.any 和 Promise.race 的区别?
答案:
| 对比项 | Promise.any | Promise.race |
|---|---|---|
| 关注点 | 第一个成功的 | 第一个 settle 的(成功或失败) |
| 对 rejection 的态度 | 忽略 rejection,继续等下一个成功 | 如果第一个 settle 是 reject,就 reject |
| 全部失败 | reject AggregateError | 返回第一个失败的结果 |
| 空数组 | reject AggregateError | 永远 pending |
| ES 版本 | ES2021 | ES2015 |
const p1 = Promise.reject('err1');
const p2 = new Promise(resolve => setTimeout(() => resolve('ok'), 100));
// race:p1 先 settle(虽然是 reject),所以整体 reject
Promise.race([p1, p2]).catch(e => console.log('race:', e)); // 'race: err1'
// any:p1 是 reject 被忽略,等到 p2 成功
Promise.any([p1, p2]).then(v => console.log('any:', v)); // 'any: ok'
Q5: Promise.all 中如何保证结果顺序?
答案:
关键在于使用索引(index)而不是 push。虽然 Promise 的完成时间是不确定的,但我们在遍历时就确定了每个 Promise 在结果数组中的位置:
for (const promise of promises) {
const index = count++; // 遍历时就确定了索引
Promise.resolve(promise).then((value) => {
results[index] = value; // 按索引赋值,不管完成顺序如何
// ...
});
}
如果使用 results.push(value),完成快的 Promise 会先 push,导致结果顺序与输入不一致。
// ❌ 错误:push 不能保证顺序
Promise.resolve(promise).then((value) => {
results.push(value); // 先完成的先 push,顺序不对
});
// ✅ 正确:按索引赋值
Promise.resolve(promise).then((value) => {
results[index] = value; // 无论完成顺序,位置固定
});
Q6: 如何实现一个带并发限制的 Promise.all?
答案:
核心思路是维护一个执行池(executing),当池中任务数达到上限时,使用 Promise.race 等待其中一个完成后再添加新任务。详细实现见上方 进阶实现 部分。
关键步骤:
- 遍历任务数组,为每个任务创建 Promise
- 将 Promise 加入执行池
- 当执行池大小 >= limit 时,
await Promise.race(executing)等待最快完成的一个 - 任务完成后从执行池中移除
- 遍历结束后,
await Promise.all(executing)等待剩余任务
更完整的调度器实现请参考 限流调度器。
Q7: 如何用 Promise.race 实现超时控制?
答案:
将实际请求与一个 setTimeout 构建的 Promise 进行竞速,谁先 settle 就采用谁的结果:
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
});
return Promise.race([promise, timeout]);
}
// 使用
try {
const data = await withTimeout(fetch('/api/slow'), 3000);
console.log(data);
} catch (error) {
if (error.message.includes('Timeout')) {
console.error('请求超时,请重试');
}
}
Promise.race 只是让我们的代码不再等待,但原始的 Promise(如 fetch)仍然在执行。如果需要真正取消请求,应该配合 AbortController 使用。
Q8: AggregateError 是什么?
答案:
AggregateError 是 ES2021 新增的错误类型,用于表示多个错误的集合。它继承自 Error,主要在 Promise.any 全部失败时使用。
interface AggregateError extends Error {
errors: any[]; // 所有错误的数组
message: string; // 错误消息
}
// 手动创建
const error = new AggregateError(
[new Error('错误1'), new Error('错误2'), new Error('错误3')],
'All promises were rejected'
);
console.log(error.message); // 'All promises were rejected'
console.log(error.errors); // [Error('错误1'), Error('错误2'), Error('错误3')]
console.log(error instanceof Error); // true
console.log(error instanceof AggregateError); // true
在 Promise.any 中的使用:
try {
await Promise.any([
Promise.reject('err1'),
Promise.reject('err2'),
]);
} catch (error) {
if (error instanceof AggregateError) {
console.log(error.errors); // ['err1', 'err2']
// 可以对每个错误单独处理
error.errors.forEach((e, i) => {
console.log(`Promise ${i} 失败原因: ${e}`);
});
}
}
Q9: 如何处理 Promise.all 中的部分失败?
答案:
有两种常见方案:
方案一:改用 Promise.allSettled
const results = await Promise.allSettled([
fetchUser(),
fetchOrders(),
fetchSettings(),
]);
// 分别处理成功和失败
const user = results[0].status === 'fulfilled' ? results[0].value : null;
const orders = results[1].status === 'fulfilled' ? results[1].value : [];
const settings = results[2].status === 'fulfilled' ? results[2].value : defaultSettings;
方案二:在 map 中 catch 返回默认值
const [user, orders, settings] = await Promise.all([
fetchUser().catch(() => null), // 失败返回 null
fetchOrders().catch(() => []), // 失败返回空数组
fetchSettings().catch(() => defaultSettings), // 失败返回默认值
]);
// 此时 Promise.all 永远不会 reject,因为每个 Promise 都有兜底
两种方案的对比:
| 对比项 | allSettled | catch 兜底 |
|---|---|---|
| 代码简洁度 | 需要解析 status 字段 | 更简洁,直接拿值 |
| 错误信息 | 保留了 reason,可以日志记录 | 错误信息在 catch 中消费了 |
| 适用场景 | 需要知道具体哪些失败了 | 只关心最终拿到值 |
Q10: 这四个方法对空数组的处理有什么区别?
答案:
| 方法 | 空数组行为 | 解释 |
|---|---|---|
Promise.all([]) | resolve([]) | 空集的「全部满足」为真(逻辑上的空真命题) |
Promise.race([]) | 永远 pending | 没有 Promise 可以 settle,所以永远等待 |
Promise.allSettled([]) | resolve([]) | 空集的「全部已完成」为真 |
Promise.any([]) | reject(AggregateError) | 没有成功的 Promise,等价于全部失败 |
// all → 立即 resolve
Promise.all([]).then(r => console.log('all:', r)); // all: []
// allSettled → 立即 resolve
Promise.allSettled([]).then(r => console.log('allSettled:', r)); // allSettled: []
// any → 立即 reject
Promise.any([]).catch(e => console.log('any:', e.message));
// any: All promises were rejected
// race → 永远 pending(以下代码永远不会输出)
Promise.race([]).then(
r => console.log('race resolved:', r),
e => console.log('race rejected:', e),
);
// (什么都不输出)
- all 和 allSettled:需要「全部」的方法,空数组 = 空集满足条件 = resolve
[] - race:需要竞争对手,空数组 = 没人参赛 = 永远 pending
- any:需要至少一个成功,空数组 = 没有成功者 = reject
Q11: 用 Promise.allSettled 如何实现 Promise.all 的效果?
答案:
async function allFromSettled<T>(promises: Promise<T>[]): Promise<T[]> {
const results = await Promise.allSettled(promises);
const firstRejected = results.find(
(r): r is PromiseRejectedResult => r.status === 'rejected'
);
if (firstRejected) {
throw firstRejected.reason; // 有失败就抛出
}
return results.map(r => (r as PromiseFulfilledResult<T>).value);
}
注意这个实现与原生 Promise.all 有一个微妙差异:原生 Promise.all 是快速失败的(遇到第一个 reject 立即结束),而上面的实现会等待所有 Promise 都 settle 后才检查是否有失败。在大多数场景下这个差异无关紧要,但在性能敏感的场景中,原生 Promise.all 响应更快。
Q12: 如何为 Promise.any 写一个 Polyfill(兼容不支持 ES2021 的环境)?
答案:
可以用 Promise.allSettled + 反转逻辑来实现:
function promiseAnyPolyfill<T>(promises: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>> {
// 思路:把每个 Promise 的 resolve/reject 反转,
// 然后用 Promise.all 检测「反转后全部成功」= 「原始全部失败」
const reversed = Array.from(promises).map(p =>
Promise.resolve(p).then(
(value) => Promise.reject(value), // 成功变失败
(reason) => Promise.resolve(reason) // 失败变成功
)
);
return Promise.all(reversed).then(
(errors) => Promise.reject(
new AggregateError(errors, 'All promises were rejected')
),
(firstValue) => Promise.resolve(firstValue as Awaited<T>)
);
}
这是一个非常巧妙的实现:通过反转每个 Promise 的成功/失败,利用 Promise.all 的快速失败特性(反转后的快速失败 = 原始的第一个成功),优雅地实现了 Promise.any 的语义。
相关链接
- MDN - Promise.all
- MDN - Promise.race
- MDN - Promise.allSettled
- MDN - Promise.any
- MDN - AggregateError
- 手写 Promise - Promise 核心实现
- 异步编程 - Promise、async/await、Generator
- 限流调度器 - 并发控制、优先级队列
- 异步串行与并行 - 异步控制模式