刷算法题、准备面试的同学肯定都见过这类题,看似简单,其实里面有很多细节值得深挖。今天把这三个方法掰开了揉碎了讲,顺便手把手带你实现一遍。
前言
Promise 的三个静态方法 all、race、allSettled 可以说是前端面试的"常客"了,特别是手写实现题,出现的频率贼高。
说实话,我第一次被问到 Promise.all 手写的时候,愣是写了个七七八八,但边界情况完全没考虑。回来一复盘,发现自己只掌握了"会用",没掌握"为什么这么设计"。
所以今天这篇文章,我不光带你写,还要帮你理解为什么这么写 ,以及有哪些坑需要避开。
先搞清楚这三个方法是干啥的
在说实现之前,先把这三个方法的语义理清楚,免得写的时候脑子一团浆糊。
Promise.all:全部成功才成功,一个失败就失败
javascript
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then(result => {
console.log(result); // [1, 2, 3]
});
简单说:等所有 Promise 都 resolve 了,才算成功。任何一个 reject 了,直接失败。
这个在什么场景用呢?比如你有个页面要同时请求用户信息、权限列表、配置数据,都拿到了才渲染------Promise.all 就派上用场了。
Promise.race:谁先settle,谁就赢
javascript
const p1 = new Promise(resolve => setTimeout(() => resolve(1), 100));
const p2 = new Promise(resolve => setTimeout(() => resolve(2), 50));
Promise.race([p1, p2]).then(result => {
console.log(result); // 2 ------ p2 更快
});
不管结果是成功还是失败,谁先有结果,谁就作为最终结果。
这个可以用来做超时控制:Promise.race([fetchData(), timeout()]),请求超时自动失败。
Promise.allSettled:不管成功失败,都要完整的结果
javascript
const p1 = Promise.resolve(1);
const p2 = Promise.reject('error');
const p3 = Promise.resolve(3);
Promise.allSettled([p1, p2, p3]).then(results => {
console.log(results);
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: 'error' },
// { status: 'fulfilled', value: 3 }
// ]
});
不管成功还是失败,所有 Promise 的结果都要收集起来。 任何一个失败不会导致其他结果丢失。
这个在需要"尽最大努力"收集结果的场景特别有用,比如批量提交表单,提交了多少、失败了多少都要知道。
开始手写:准备工作
在写这三个方法之前,先搞个辅助函数:
javascript
// 把任意值转成 Promise
// 如果已经是 Promise 就直接返回,如果不是就包装成 resolved Promise
const toPromise = (value) => Promise.resolve(value);
这个函数很关键,因为我们后面要处理的参数不一定是 Promise,可能是普通值。
一、手写 Promise.all
1.1 核心思路
Promise.all 的逻辑其实很清晰:
- 接收一个 Promise 数组
- 等所有 Promise 都 resolve 了,resolve 一个包含所有结果的数组
- 任何一个 reject 了,直接 reject
1.2 逐步实现
第一步:参数校验
javascript
function myPromiseAll(promises) {
// 首先判断是不是数组,不是的话直接报错
if (!Array.isArray(promises)) {
return Promise.reject(new TypeError('Argument is not iterable'));
}
// ... 后续逻辑
}
这个校验其实很少考到,但写上总比不写强,显得你考虑得周全。
第二步:处理空数组
javascript
return new Promise((resolve, reject) => {
const len = promises.length;
// 空数组直接返回空数组,这个边界情况很容易忽略
if (len === 0) {
return resolve([]);
}
// ... 后续逻辑
});
你可能觉得空数组有什么好处理的?但如果不做这个判断,后面的逻辑会有问题------空数组的时候 completedCount 永远是 0,resolve 永远不会被调用。
第三步:收集结果
javascript
const results = new Array(len); // 预分配结果数组,保持顺序
let completedCount = 0;
promises.forEach((item, index) => {
toPromise(item) // 先转成 Promise
.then((value) => {
results[index] = value; // 按原顺序存储结果
completedCount++;
if (completedCount === len) {
resolve(results); // 全部完成才 resolve
}
})
.catch(reject); // 任何一个失败就 reject
});
几个关键点:
- 用
index而不是 push,是为了保持结果顺序------Promise.race 用不着这个,但 Promise.all 必须保证顺序 - 用
.catch(reject)而不是第二个参数,是因为 reject 在这儿的语义是一致的
1.3 完整代码
javascript
function myPromiseAll(promises) {
if (!Array.isArray(promises)) {
return Promise.reject(new TypeError('Argument is not iterable'));
}
return new Promise((resolve, reject) => {
const len = promises.length;
if (len === 0) {
return resolve([]);
}
const results = new Array(len);
let completedCount = 0;
promises.forEach((item, index) => {
toPromise(item)
.then((value) => {
results[index] = value;
completedCount++;
if (completedCount === len) {
resolve(results);
}
})
.catch(reject);
});
});
}
1.4 测试一把
javascript
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = new Promise((_, reject) => setTimeout(() => reject('error'), 100));
myPromiseAll([p1, p2]).then(console.log); // [1, 2]
myPromiseAll([p1, p2, p3]).catch(console.error); // 'error'
myPromiseAll([]).then(console.log); // []
myPromiseAll([1, 2, 3]).then(console.log); // [1, 2, 3] 普通值也能处理
二、手写 Promise.race
2.1 核心思路
Promise.race 就简单多了:
- 接收一个 Promise 数组
- 哪个 Promise 先 settle(resolve 或 reject),就以那个结果为最终结果
2.2 实现
说实话,这个比 Promise.all 简单太多了:
javascript
function myPromiseRace(promises) {
if (!Array.isArray(promises)) {
return Promise.reject(new TypeError('Argument is not iterable'));
}
return new Promise((resolve, reject) => {
promises.forEach((item) => {
// 任何一个 settle 了,就 resolve 或 reject
toPromise(item).then(resolve, reject);
});
});
}
注意这里 .then(resolve, reject) 的写法------第一个参数是 resolve 的回调,第二个是 reject 的回调。因为我们希望不管成功还是失败,只要先到就行。
2.3 空数组的情况
javascript
myPromiseRace([]).then(console.log); // 这行代码永远不会执行
空数组调用 race 会怎样?实际上它永远不会 settle,原生的 Promise 也是这样。所以我们不需要额外处理空数组,让浏览器自行决定就好。
2.4 完整代码
javascript
function myPromiseRace(promises) {
if (!Array.isArray(promises)) {
return Promise.reject(new TypeError('Argument is not iterable'));
}
return new Promise((resolve, reject) => {
promises.forEach((item) => {
toPromise(item).then(resolve, reject);
});
});
}
2.5 常见面试题:超时控制
javascript
function withTimeout(promise, timeout) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), timeout)
)
]);
}
// 使用
withTimeout(fetch('/api/data'), 3000)
.then(data => console.log(data))
.catch(err => console.error(err.message)); // 可能是 "timeout"
三、手写 Promise.allSettled
3.1 核心思路
Promise.allSettled 的特点是不抛弃任何一个 Promise:
- 接收一个 Promise 数组
- 等待所有 Promise 都 settle 了(不管成功还是失败)
- 返回一个数组,每个元素都标注了状态和结果/原因
3.2 实现
这个比 Promise.all 多了一步:不管成功失败,都要记录下来。
javascript
function myPromiseAllSettled(promises) {
if (!Array.isArray(promises)) {
return Promise.reject(new TypeError('Argument is not iterable'));
}
return new Promise((resolve) => {
const len = promises.length;
if (len === 0) {
return resolve([]);
}
const results = new Array(len);
let completedCount = 0;
promises.forEach((item, index) => {
toPromise(item)
.then(
(value) => {
results[index] = { status: 'fulfilled', value };
},
(reason) => {
results[index] = { status: 'rejected', reason };
}
)
.finally(() => {
completedCount++;
if (completedCount === len) {
resolve(results);
}
});
});
});
}
关键点:
- 用
.then()的两个参数分别处理成功和失败,而不是.catch() - 用
.finally()来计数,因为不管是成功还是失败,都要算"完成了"
3.3 测试一把
javascript
const p1 = Promise.resolve(1);
const p2 = Promise.reject('oops');
const p3 = new Promise(resolve => setTimeout(() => resolve(3), 50));
myPromiseAllSettled([p1, p2, p3]).then(results => {
console.log(results);
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: 'oops' },
// { status: 'fulfilled', value: 3 }
// ]
});
3.4 实际应用场景
批量提交表单,想知道每个表单项的提交结果:
javascript
async function submitAllForms(forms) {
const promises = forms.map(form => submitForm(form));
const results = await Promise.allSettled(promises);
const success = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
console.log(`成功: ${success}, 失败: ${failed}`);
return results;
}
四、三者对比总结
| 方法 | 成功条件 | 失败条件 | 返回值结构 |
|---|---|---|---|
Promise.all |
全部 resolve | 任何一个 reject | 普通数组 [val1, val2] |
Promise.race |
任何一个先 settle | 任何一个先 settle | 单个值 val |
Promise.allSettled |
全部 settle | 不会失败(总会 resolve) | 对象数组 [{status, value/reason}] |
一句话记忆:
all:一个都不能少,一个都不能输race:谁快谁赢,不管输赢allSettled:不管成功失败,我全都要
五、完整代码汇总
javascript
// 辅助函数:将任意值转换为 Promise
const toPromise = (value) => Promise.resolve(value);
// 1. Promise.all
function myPromiseAll(promises) {
if (!Array.isArray(promises)) {
return Promise.reject(new TypeError('Argument is not iterable'));
}
return new Promise((resolve, reject) => {
const len = promises.length;
if (len === 0) {
return resolve([]);
}
const results = new Array(len);
let completedCount = 0;
promises.forEach((item, index) => {
toPromise(item)
.then((value) => {
results[index] = value;
completedCount++;
if (completedCount === len) {
resolve(results);
}
})
.catch(reject);
});
});
}
// 2. Promise.race
function myPromiseRace(promises) {
if (!Array.isArray(promises)) {
return Promise.reject(new TypeError('Argument is not iterable'));
}
return new Promise((resolve, reject) => {
promises.forEach((item) => {
toPromise(item).then(resolve, reject);
});
});
}
// 3. Promise.allSettled
function myPromiseAllSettled(promises) {
if (!Array.isArray(promises)) {
return Promise.reject(new TypeError('Argument is not iterable'));
}
return new Promise((resolve) => {
const len = promises.length;
if (len === 0) {
return resolve([]);
}
const results = new Array(len);
let completedCount = 0;
promises.forEach((item, index) => {
toPromise(item)
.then(
(value) => {
results[index] = { status: 'fulfilled', value };
},
(reason) => {
results[index] = { status: 'rejected', reason };
}
)
.finally(() => {
completedCount++;
if (completedCount === len) {
resolve(results);
}
});
});
});
}
写在最后
手写 Promise 静态方法其实不算难,关键是搞清楚语义 和边界情况:
- 参数校验------是不是数组
- 空数组处理------要不要特殊处理
- 顺序保持------Promise.all 需要用 index 而不是 push
- 失败策略------all 一个失败全失败,race 谁先谁赢,allSettled 永不失败
面试的时候能写出来这些细节,面试官肯定会对你刮目相看。
觉得有帮助的话,点个赞呗 有问题评论区见~