跳到主要内容

手写 Promise 静态方法

问题

手写实现 Promise.allPromise.racePromise.allSettledPromise.any 四个静态方法。

答案

Promise 的四个静态方法是前端面试中出现频率极高的手写题。它们都接收一个 可迭代对象(Iterable),但在处理成功、失败的策略上各不相同。理解它们的差异和实现细节,不仅有助于面试,也能让你在实际开发中选择最合适的并发控制方案。

前置知识

在阅读本文之前,建议先了解 手写 Promise 中 Promise 的核心实现(状态管理、链式调用),以及 异步编程 中 Promise 的基本用法。


四个方法对比总览

方法全部成功部分失败全部失败返回值ES 版本
Promise.allresolve 结果数组第一个 reject第一个 rejectT[] / 第一个错误ES2015
Promise.race第一个 settle 的值第一个 settle 的值第一个 settle 的值第一个结果ES2015
Promise.allSettledsettled 状态数组settled 状态数组settled 状态数组PromiseSettledResult<T>[]ES2020
Promise.any第一个 resolve 的值第一个 resolve 的值AggregateError第一个成功值ES2021

一、Promise.all

Promise.all 接收一个可迭代对象,当所有 Promise 都成功时返回结果数组,任何一个失败则立即 reject(快速失败策略)。

promiseAll 实现
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 空数组
});
}
关键细节
  1. 保持顺序:用 index 记录每个 Promise 的位置,而不是 push。虽然 Promise 完成的时间不确定,但结果数组的顺序必须与输入一致。
  2. 空数组处理Promise.all([]) 会同步 resolve 一个空数组 []
  3. 非 Promise 值:通过 Promise.resolve(promise) 将普通值包装为 Promise,确保统一处理。
  4. 快速失败:只要有一个 reject,整个 Promise.all 就立即 reject,其他 Promise 的结果会被忽略(但不会被取消,它们仍会继续执行)。

使用示例

Promise.all 使用
// 全部成功
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 结果。

promiseRace 实现
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
});
}
为什么多次调用 resolve/reject 没问题?

Promise 的状态一旦改变就不可逆转。第一个 settle 的 Promise 调用 resolvereject 后,后续的调用会被静默忽略。这是 Promise 规范保证的行为,不需要额外加锁或判断。

空 Iterable 的陷阱

Promise.race([]) 返回的 Promise 永远处于 pending 状态,永远不会 resolve 或 reject。这可能导致内存泄漏或逻辑死锁,使用时务必确保传入的数组非空。

使用示例

Promise.race 使用
// 竞速:返回最快的
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

promiseAllSettled 实现
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

使用示例

Promise.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

promiseAny 实现
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'));
}
});
}
注意事项
  1. Promise.race 的区别race 返回第一个 settle 的结果(不管成功失败),any 会忽略 rejection,只关心第一个成功的。
  2. AggregateError:ES2021 新增的错误类型,继承自 Error,有一个 errors 属性是所有错误的数组。
  3. 空 IterablePromise.any([]) 会直接 reject 一个 AggregateError,与 race 的永远 pending 不同。

使用示例

Promise.any 使用
// 返回第一个成功的
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 空数组的危险

Promise.race([]) 永远不会 settle,如果你在 await 它,代码会永远挂起。在实际开发中,务必在调用 Promise.race 前检查数组是否为空。


六、实际应用场景

场景:并行请求,全部成功才继续

页面初始化:并行加载多个接口
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 };
}

七、进阶实现

1. 带并发限制的 Promise.all

标准 Promise.all 会同时启动所有 Promise,对于大量请求可能造成服务端压力。结合 限流调度器 的思路,实现一个带并发限制的版本:

promiseAllWithLimit 实现
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

promiseRaceWithTimeout 实现
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

cancellablePromiseAll 实现
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

答案

核心要点:

  1. 接收 Iterable,用 for...of 遍历
  2. index 保证结果顺序(闭包捕获索引)
  3. 用计数器判断全部完成
  4. 任一 reject 立即 reject(快速失败)
  5. 空数组直接 resolve []
  6. 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 是四个方法中实现最简单的:

面试版 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.allPromise.allSettled
失败处理任一失败立即 reject(快速失败)等待全部完成,永远 resolve
返回值成功值数组 T[]状态对象数组 PromiseSettledResult<T>[]
适用场景全部必须成功(页面加载)允许部分失败(批量操作)
ES 版本ES2015ES2020
实际场景对比
// 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.anyPromise.race
关注点第一个成功第一个 settle 的(成功或失败)
对 rejection 的态度忽略 rejection,继续等下一个成功如果第一个 settle 是 reject,就 reject
全部失败reject AggregateError返回第一个失败的结果
空数组reject AggregateError永远 pending
ES 版本ES2021ES2015
行为差异示例
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 等待其中一个完成后再添加新任务。详细实现见上方 进阶实现 部分。

关键步骤:

  1. 遍历任务数组,为每个任务创建 Promise
  2. 将 Promise 加入执行池
  3. 当执行池大小 >= limit 时,await Promise.race(executing) 等待最快完成的一个
  4. 任务完成后从执行池中移除
  5. 遍历结束后,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 全部失败时使用。

AggregateError 结构
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 中的使用

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

方案一: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 返回默认值

方案二:catch 返回默认值
const [user, orders, settings] = await Promise.all([
fetchUser().catch(() => null), // 失败返回 null
fetchOrders().catch(() => []), // 失败返回空数组
fetchSettings().catch(() => defaultSettings), // 失败返回默认值
]);
// 此时 Promise.all 永远不会 reject,因为每个 Promise 都有兜底

两种方案的对比:

对比项allSettledcatch 兜底
代码简洁度需要解析 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),
);
// (什么都不输出)
面试记忆技巧
  • allallSettled:需要「全部」的方法,空数组 = 空集满足条件 = resolve []
  • race:需要竞争对手,空数组 = 没人参赛 = 永远 pending
  • any:需要至少一个成功,空数组 = 没有成功者 = reject

Q11: 用 Promise.allSettled 如何实现 Promise.all 的效果?

答案

用 allSettled 模拟 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 + 反转逻辑来实现:

Promise.any Polyfill
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 的语义。

相关链接