面试官让我手写 Promise.all / Promise.race / Promise.allSettled,我直接水灵灵地写出来了

刷算法题、准备面试的同学肯定都见过这类题,看似简单,其实里面有很多细节值得深挖。今天把这三个方法掰开了揉碎了讲,顺便手把手带你实现一遍。


前言

Promise 的三个静态方法 allraceallSettled 可以说是前端面试的"常客"了,特别是手写实现题,出现的频率贼高。

说实话,我第一次被问到 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 的逻辑其实很清晰:

  1. 接收一个 Promise 数组
  2. 等所有 Promise 都 resolve 了,resolve 一个包含所有结果的数组
  3. 任何一个 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 就简单多了:

  1. 接收一个 Promise 数组
  2. 哪个 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

  1. 接收一个 Promise 数组
  2. 等待所有 Promise 都 settle 了(不管成功还是失败)
  3. 返回一个数组,每个元素都标注了状态和结果/原因

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 静态方法其实不算难,关键是搞清楚语义边界情况

  1. 参数校验------是不是数组
  2. 空数组处理------要不要特殊处理
  3. 顺序保持------Promise.all 需要用 index 而不是 push
  4. 失败策略------all 一个失败全失败,race 谁先谁赢,allSettled 永不失败

面试的时候能写出来这些细节,面试官肯定会对你刮目相看。

觉得有帮助的话,点个赞呗 有问题评论区见~

相关推荐
gogoing1 小时前
webpack 的性能优化
前端·javascript
gogoing1 小时前
Node.js 模块查找策略(require 完整流程)
javascript·node.js
gogoing1 小时前
await fetch() 的两阶段设计
前端·javascript
gogoing1 小时前
前端首屏加载优化
前端·javascript
gogoing1 小时前
重排与重绘
前端·javascript
逻辑驱动的ken1 小时前
Java高频考点场景题24
java·开发语言·面试·职场和发展·求职招聘
Fox爱分享2 小时前
字节二面:10亿数据毫秒级查手机尾号后4位,答不出“异构索引”直接挂?
java·后端·面试
WaywardOne2 小时前
Flutter面试事件队列,微任务队列以及事件循环相关问题及回答
flutter·面试
折哥的程序人生 · 物流技术专研2 小时前
《Java面试85题图解版(二)》进阶深化上篇:并发编程 + JVM
java·开发语言·后端·面试