手写 Promise.all、race、any:不到 30 行代码,解决并发异步的所有姿势

一、这三个方法解决什么问题?

方法 输入 输出 典型场景
Promise.all 多个 Promise 所有成功 -> 结果数组;任一失败 -> 立即 reject 多个独立请求,需要全部结果
Promise.race 多个 Promise 最先 settled 的结果(成功或失败) 超时控制、请求竞速
Promise.any 多个 Promise 任一成功 -> 该成功值;全部失败 -> AggregateError 多个备用接口,取最快成功的

二、手写 Promise.all

核心逻辑

  1. 返回一个新的 Promise
  2. 遍历所有 promise,用 Promise.resolve 包一层(支持非 promise 值)
  3. 用计数器记录完成个数,按顺序存储结果
  4. 任何一个失败则直接 reject
javascript 复制代码
Promise.myAll = function(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('Argument must be an array'));
    }
    const results = [];
    let completed = 0;
    const total = promises.length;

    if (total === 0) {
      return resolve(results);
    }

    promises.forEach((p, index) => {
      Promise.resolve(p).then(
        (value) => {
          results[index] = value;
          completed++;
          if (completed === total) {
            resolve(results);
          }
        },
        (reason) => {
          reject(reason);
        }
      );
    });
  });
};

测试

javascript 复制代码
const p1 = Promise.resolve(1);
const p2 = 2;
const p3 = new Promise((res) => setTimeout(() => res(3), 100));

Promise.myAll([p1, p2, p3]).then(console.log); // [1, 2, 3](约100ms后)

三、手写 Promise.race

核心逻辑:谁先 settled(成功或失败),就 resolve/reject 谁。

javascript 复制代码
Promise.myRace = function(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('Argument must be an array'));
    }
    promises.forEach((p) => {
      Promise.resolve(p).then(resolve, reject);
    });
  });
};

测试

javascript 复制代码
const slow = new Promise(res => setTimeout(() => res('慢'), 100));
const fast = new Promise(res => setTimeout(() => res('快'), 20));
Promise.myRace([slow, fast]).then(console.log); // '快'

四、手写 Promise.any

核心逻辑

  1. 收集所有失败原因
  2. 只要有一个成功,就 resolve 该值
  3. 全部失败时,reject 一个 AggregateError
javascript 复制代码
Promise.myAny = function(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('Argument must be an array'));
    }
    const errors = [];
    let rejectedCount = 0;
    const total = promises.length;

    if (total === 0) {
      return reject(new AggregateError([], 'All promises were rejected'));
    }

    promises.forEach((p, index) => {
      Promise.resolve(p).then(
        (value) => {
          resolve(value);
        },
        (reason) => {
          errors[index] = reason;
          rejectedCount++;
          if (rejectedCount === total) {
            reject(new AggregateError(errors, 'All promises were rejected'));
          }
        }
      );
    });
  });
};

测试

javascript 复制代码
const p1 = Promise.reject('错误1');
const p2 = new Promise(res => setTimeout(() => res('成功'), 50));
const p3 = Promise.reject('错误3');

Promise.myAny([p1, p2, p3]).then(console.log); // '成功'(50ms后)

五、3 个容易忽略的边界情况

边界1:传入空数组

  • Promise.all([]) → 立即 resolve []
  • Promise.race([]) → 永远 pending(不会 resolve/reject)
  • Promise.any([]) → 立即 reject AggregateError

我们的实现已正确处理。

边界2:传入非 Promise 值

Promise.resolve 自动包装,无需额外处理。

边界3:数组包含已 settled 的 Promise

不影响逻辑,依然按顺序等待或竞速。


六、完整代码(可直接复制)

javascript 复制代码
// Promise.all
Promise.myAll = function(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) reject(new TypeError('Expected array'));
    const results = [];
    let completed = 0;
    if (promises.length === 0) resolve(results);
    promises.forEach((p, i) => {
      Promise.resolve(p).then(
        v => { results[i] = v; completed++; if (completed === promises.length) resolve(results); },
        e => reject(e)
      );
    });
  });
};

// Promise.race
Promise.myRace = function(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) reject(new TypeError('Expected array'));
    promises.forEach(p => Promise.resolve(p).then(resolve, reject));
  });
};

// Promise.any
Promise.myAny = function(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) reject(new TypeError('Expected array'));
    const errors = [];
    let rejected = 0;
    if (promises.length === 0) reject(new AggregateError([], 'All rejected'));
    promises.forEach((p, i) => {
      Promise.resolve(p).then(
        v => resolve(v),
        e => { errors[i] = e; rejected++; if (rejected === promises.length) reject(new AggregateError(errors, 'All rejected')); }
      );
    });
  });
};

七、总结

  • Promise.all:全成功才成功,一失败就失败
  • Promise.race:谁先 settled 就输出谁
  • Promise.any:一成功就成功,全失败才失败
  • 三个方法核心都是 遍历 + 计数器 + 按序存储
  • 边界情况:空数组、非 Promise 值、已 settled 项

文中代码已测试,可用于生产环境 polyfill。下一篇写 Promise.allSettledPromise.withResolvers

讨论 :你在项目中用过 Promise.any 吗?一般用在哪里?欢迎评论区分享。

相关推荐
子兮曰2 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰2 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万2 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝2 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋2 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
晨米酱2 天前
AGENTS.md:Agent 的上下文策略层
面试·架构·agent
卡布鲁2 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
李少兄2 天前
JavaScript 隐式全局变量解析
javascript
彧azz2 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
汉堡大王95272 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端