JS 入门通关手册(42):Promise 并发控制(all/race/allSettled/any 手写 + 实战)

摘要

本文聚焦 Promise 核心并发控制方法,详细讲解 Promise.all、Promise.race、Promise.allSettled、Promise.any 的用法、区别、底层原理,并提供可直接用于面试的手写实现,结合接口并发请求、超时控制、容错处理等真实业务场景,帮你彻底掌握 Promise 并发编程,轻松应对前端面试与项目开发。


一、前言:为什么需要 Promise 并发控制?

在实际项目中,我们经常需要同时处理多个异步操作(如同时请求多个接口、并行获取多份数据),如果逐个等待异步操作完成,会严重影响页面性能和用户体验。

Promise 提供了 4 种核心并发控制方法,专门解决 "多异步并行执行" 的问题,也是前端面试的高频考点(手写 + 用法 + 区别,必问)。

核心需求:

  1. 多个异步操作同时执行,提高效率
  2. 控制多个异步的执行结果(全部成功、任意一个成功、全部结束、任意一个完成)
  3. 处理并发中的错误(部分失败、全部失败、超时失败)

二、Promise.all(最常用,全部成功才成功)

1. 核心定义

Promise.all (iterable):接收一个可迭代对象 (如数组),里面包含多个 Promise;只有所有 Promise 都成功(fulfilled) ,返回的新 Promise 才会成功,结果是所有 Promise 结果的数组;只要有一个失败(rejected),就会立即返回失败,失败原因是第一个失败的 Promise 的错误。

一句话记忆:"全部成功才成功,一个失败就失败"

2. 基础用法(实战必用)

javascript

运行

javascript 复制代码
// 模拟3个接口请求(异步操作)
const request1 = () => Promise.resolve("接口1返回数据");
const request2 = () => Promise.resolve("接口2返回数据");
const request3 = () => Promise.resolve("接口3返回数据");

// 并发执行3个请求
Promise.all([request1(), request2(), request3()])
  .then((res) => {
    console.log("所有请求成功:", res); // [接口1数据, 接口2数据, 接口3数据]
  })
  .catch((err) => {
    console.log("第一个失败的错误:", err);
  });

// 测试失败场景(有一个请求失败)
const request4 = () => Promise.reject(new Error("接口4失败"));
Promise.all([request1(), request4(), request3()])
  .then((res) => console.log(res))
  .catch((err) => console.log(err.message)); // 接口4失败

3. 关键注意点(面试避坑)

  • 传入的可迭代对象中,非 Promise 元素会被自动转为 Promise.resolve(元素)(如 [1, 2, Promise.resolve(3)] 会转为 [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]
  • 结果数组的顺序和传入的 Promise 顺序一致(即使某个 Promise 先完成,也会按传入顺序排列结果)
  • 一旦有一个 Promise 失败,会立即触发 catch,后续其他 Promise 的结果会被忽略

4. 手写 Promise.all(面试必写)

javascript

运行

javascript 复制代码
// 手写 Promise.all
Promise.myAll = function (iterable) {
  // 返回一个新 Promise
  return new Promise((resolve, reject) => {
    // 1. 处理边界:如果传入空数组,直接成功返回空数组
    if (iterable.length === 0) {
      resolve([]);
      return;
    }

    const result = []; // 存储所有成功结果
    let count = 0; // 记录已完成的 Promise 数量

    // 2. 遍历所有可迭代对象中的元素
    for (let i = 0; i < iterable.length; i++) {
      // 处理非 Promise 元素,转为成功的 Promise
      Promise.resolve(iterable[i])
        .then((res) => {
          result[i] = res; // 按顺序存储结果
          count++;

          // 3. 所有 Promise 都完成,resolve 结果数组
          if (count === iterable.length) {
            resolve(result);
          }
        })
        .catch((err) => {
          // 4. 有一个失败,立即 reject
          reject(err);
        });
    }
  });
};

// 测试手写 myAll
Promise.myAll([request1(), request2(), request3()])
  .then((res) => console.log("手写myAll成功:", res))
  .catch((err) => console.log(err));

三、Promise.race(最快完成的结果,无论成功失败)

1. 核心定义

Promise.race (iterable):接收一个可迭代对象,返回一个新 Promise;哪个 Promise 最先完成(无论成功还是失败),就返回哪个 Promise 的结果 / 错误。

一句话记忆:"比速度,谁先完成就取谁,不管成功失败"

2. 基础用法(实战场景:超时控制)

最常用场景:接口超时处理(如果接口在规定时间内未响应,就判定为失败)

javascript

运行

javascript 复制代码
// 模拟接口请求(延迟3秒)
const request = () =>
  new Promise((resolve) => setTimeout(() => resolve("接口请求成功"), 3000));

// 模拟超时(延迟2秒,返回失败)
const timeout = () =>
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error("接口请求超时")), 2000)
  );

// 用 race 实现超时控制:谁先完成取谁
Promise.race([request(), timeout()])
  .then((res) => console.log(res))
  .catch((err) => console.log(err.message)); // 接口请求超时(timeout先完成)

3. 关键注意点(面试避坑)

  • 只要有一个 Promise 完成(fulfilled 或 rejected),就会立即返回结果,后续其他 Promise 的结果会被忽略
  • 传入的非 Promise 元素,会被转为成功的 Promise,且会立即完成(因为同步元素,比异步 Promise 快)
  • 常用于:超时控制、竞争条件(如多个请求取最快的一个)

4. 手写 Promise.race(面试必写)

javascript

运行

javascript 复制代码
// 手写 Promise.race
Promise.myRace = function (iterable) {
  return new Promise((resolve, reject) => {
    // 遍历所有元素,谁先完成就 resolve/reject
    for (const item of iterable) {
      // 处理非 Promise 元素
      Promise.resolve(item)
        .then((res) => {
          resolve(res); // 第一个成功,立即返回
        })
        .catch((err) => {
          reject(err); // 第一个失败,立即返回
        });
    }
  });
};

// 测试手写 myRace(超时场景)
Promise.myRace([request(), timeout()])
  .then((res) => console.log(res))
  .catch((err) => console.log("手写myRace失败:", err.message));

四、Promise.allSettled(所有操作都结束,无论成功失败)

1. 核心定义

Promise.allSettled (iterable):接收一个可迭代对象,返回一个新 Promise;只有所有 Promise 都结束(无论成功还是失败),才会返回成功,结果是一个数组,每个元素包含对应 Promise 的状态和结果 / 错误。

一句话记忆:"无论成功失败,都要等所有操作结束,返回所有结果"

2. 基础用法(实战场景:批量请求,需知道所有结果)

场景:批量获取多个接口数据,即使部分接口失败,也需要知道所有接口的执行结果(如批量导出、批量统计)

javascript

运行

javascript 复制代码
const request1 = () => Promise.resolve("接口1成功");
const request2 = () => Promise.reject(new Error("接口2失败"));
const request3 = () => Promise.resolve("接口3成功");

// 等待所有请求结束,获取所有结果
Promise.allSettled([request1(), request2(), request3()])
  .then((results) => {
    console.log("所有请求结束:", results);
    // 结果数组格式:
    // [
    //   { status: 'fulfilled', value: '接口1成功' },
    //   { status: 'rejected', reason: Error: 接口2失败 },
    //   { status: 'fulfilled', value: '接口3成功' }
    // ]

    // 筛选成功/失败的请求
    const success = results.filter((item) => item.status === "fulfilled");
    const fail = results.filter((item) => item.status === "rejected");
    console.log("成功的请求:", success);
    console.log("失败的请求:", fail);
  });

3. 关键注意点(面试避坑)

  • 无论传入的 Promise 成功还是失败,返回的新 Promise 永远是 fulfilled 状态(除非传入的可迭代对象有异常)
  • 结果数组中,每个元素只有两种格式:
    • 成功:{ status: 'fulfilled', value: 结果 }
    • 失败:{ status: 'rejected', reason: 错误 }
  • 和 Promise.all 的区别:all 有一个失败就立即失败,allSettled 会等待所有结束,返回所有结果

4. 手写 Promise.allSettled(面试加分)

javascript

运行

javascript 复制代码
// 手写 Promise.allSettled
Promise.myAllSettled = function (iterable) {
  return new Promise((resolve) => {
    if (iterable.length === 0) {
      resolve([]);
      return;
    }

    const results = [];
    let count = 0;

    for (let i = 0; i < iterable.length; i++) {
      Promise.resolve(iterable[i])
        .then((value) => {
          // 成功:存储状态和结果
          results[i] = { status: "fulfilled", value };
        })
        .catch((reason) => {
          // 失败:存储状态和错误
          results[i] = { status: "rejected", reason };
        })
        .finally(() => {
          count++;
          // 所有操作结束,resolve 结果数组
          if (count === iterable.length) {
            resolve(results);
          }
        });
    }
  });
};

// 测试手写 myAllSettled
Promise.myAllSettled([request1(), request2(), request3()])
  .then((results) => console.log("手写myAllSettled:", results));

五、Promise.any(任意一个成功就成功,全部失败才失败)

1. 核心定义

Promise.any (iterable):接收一个可迭代对象,返回一个新 Promise;只要有一个 Promise 成功(fulfilled) ,就立即返回成功,结果是第一个成功的 Promise 的结果;只有所有 Promise 都失败(rejected),才会返回失败,错误是所有失败原因的集合。

一句话记忆:"任意一个成功就成功,全部失败才失败"(和 all 相反,和 race 类似但只取成功的)

2. 基础用法(实战场景:多源请求容错)

场景:同一个接口,请求多个服务器(备用服务器),只要有一个服务器响应成功,就使用该结果,全部失败才提示错误

javascript

运行

javascript 复制代码
// 模拟3个服务器请求(2个失败,1个成功)
const requestServer1 = () => Promise.reject(new Error("服务器1失败"));
const requestServer2 = () => Promise.resolve("服务器2请求成功");
const requestServer3 = () => Promise.reject(new Error("服务器3失败"));

// 只要有一个成功,就返回成功结果
Promise.any([requestServer1(), requestServer2(), requestServer3()])
  .then((res) => console.log("请求成功:", res)) // 服务器2请求成功
  .catch((err) => {
    console.log("所有服务器都失败:", err.errors); // 所有失败原因的数组
  });

// 测试全部失败场景
const requestA = () => Promise.reject(new Error("A失败"));
const requestB = () => Promise.reject(new Error("B失败"));
Promise.any([requestA(), requestB()])
  .then((res) => console.log(res))
  .catch((err) => console.log(err.errors)); // [Error: A失败, Error: B失败]

3. 关键注意点(面试避坑)

  • 和 Promise.race 的区别:race 取 "最先完成" 的(无论成功失败),any 取 "最先成功" 的(失败的会忽略,直到有一个成功)
  • 和 Promise.all 的区别:all 需全部成功,any 只需一个成功
  • 若所有 Promise 都失败,返回的错误是 AggregateError 类型,包含 errors 属性(所有失败原因的数组)

4. 手写 Promise.any(面试进阶)

javascript

运行

javascript 复制代码
// 手写 Promise.any
Promise.myAny = function (iterable) {
  return new Promise((resolve, reject) => {
    if (iterable.length === 0) {
      reject(new AggregateError([], "All promises were rejected"));
      return;
    }

    const errors = []; // 存储所有失败原因
    let count = 0;

    for (let i = 0; i < iterable.length; i++) {
      Promise.resolve(iterable[i])
        .then((res) => {
          // 第一个成功,立即 resolve
          resolve(res);
        })
        .catch((err) => {
          errors[i] = err;
          count++;

          // 所有都失败,reject AggregateError
          if (count === iterable.length) {
            reject(new AggregateError(errors, "All promises were rejected"));
          }
        });
    }
  });
};

// 测试手写 myAny
Promise.myAny([requestServer1(), requestServer2(), requestServer3()])
  .then((res) => console.log("手写myAny成功:", res))
  .catch((err) => console.log("手写myAny失败:", err.errors));

六、4 种并发方法对比(面试必背)

表格

方法 核心逻辑 成功条件 失败条件 结果格式 适用场景
Promise.all 全部完成 所有 Promise 成功 任意一个失败 成功结果数组(顺序和传入一致) 多接口并行,需所有数据才渲染
Promise.race 最先完成 最先完成的 Promise 成功 最先完成的 Promise 失败 最先完成的结果 / 错误 超时控制、竞争条件
Promise.allSettled 全部完成 所有 Promise 都结束(无论成败) 无(永远 fulfilled) 每个元素包含 status 和 value/reason 批量请求,需知道所有结果
Promise.any 任意一个成功 任意一个 Promise 成功 所有 Promise 都失败 第一个成功的结果 多源容错(备用服务器、备用接口)

面试口诀(快速记忆)

  • all:全成则成,一败则败
  • race:谁快取谁,不分成败
  • allSettled:尽收眼底,无论成败
  • any:一成则成,全败则败

七、实战场景汇总(真实项目常用)

场景 1:多接口并行请求(all)

javascript

运行

javascript 复制代码
// 同时请求用户、订单、商品数据,全部成功后渲染页面
const getUser = () => axios.get("/api/user");
const getOrder = () => axios.get("/api/order");
const getGoods = () => axios.get("/api/goods");

Promise.all([getUser(), getOrder(), getGoods()])
  .then(([userRes, orderRes, goodsRes]) => {
    // 解构结果,渲染页面
    renderUser(userRes.data);
    renderOrder(orderRes.data);
    renderGoods(goodsRes.data);
  })
  .catch((err) => console.log("请求失败:", err));

场景 2:接口超时控制(race)

javascript

运行

javascript 复制代码
// 封装带超时的请求函数
function requestWithTimeout(url, timeout = 3000) {
  const request = axios.get(url);
  const timeoutPromise = new Promise((_, reject) =>
    setTimeout(() => reject(new Error("超时")), timeout)
  );
  return Promise.race([request, timeoutPromise]);
}

// 使用
requestWithTimeout("/api/data")
  .then((res) => console.log(res.data))
  .catch((err) => console.log(err.message));

场景 3:批量导出(allSettled)

javascript

运行

javascript 复制代码
// 批量导出多个表格数据,即使部分失败,也记录失败原因
const exportTable1 = () => Promise.resolve("表格1导出成功");
const exportTable2 = () => Promise.reject(new Error("表格2导出失败"));
const exportTable3 = () => Promise.resolve("表格3导出成功");

Promise.allSettled([exportTable1(), exportTable2(), exportTable3()])
  .then((results) => {
    results.forEach((item, index) => {
      if (item.status === "fulfilled") {
        console.log(`表格${index+1}:${item.value}`);
      } else {
        console.log(`表格${index+1}:${item.reason.message}`);
      }
    });
  });

场景 4:多源请求容错(any)

javascript

运行

javascript 复制代码
// 请求3个CDN资源,只要有一个成功就使用
const getCDN1 = () => axios.get("https://cdn1.com/resource");
const getCDN2 = () => axios.get("https://cdn2.com/resource");
const getCDN3 = () => axios.get("https://cdn3.com/resource");

Promise.any([getCDN1(), getCDN2(), getCDN3()])
  .then((res) => console.log("使用资源:", res.data))
  .catch((err) => console.log("所有CDN都失败:", err.errors));

八、高频面试题(必背标准答案)

  1. **Promise.all、race、allSettled、any 的区别?**答:all 全成则成、一败则败;race 最先完成则返回(不分成败);allSettled 等待所有结束,返回所有结果;any 一成则成、全败则败。

  2. **手写 Promise.all 的核心思路是什么?**答:遍历传入的可迭代对象,将非 Promise 转为成功 Promise,收集所有成功结果,当所有 Promise 完成时 resolve 结果数组,有一个失败则立即 reject。

  3. **如何用 Promise.race 实现接口超时控制?**答:创建两个 Promise,一个是接口请求,一个是超时定时器(reject),用 race 包裹,谁先完成就取谁的结果,超时定时器先完成则判定为请求超时。

  4. **Promise.allSettled 和 Promise.all 的区别?**答:all 有一个失败就立即失败,不等待其他 Promise;allSettled 会等待所有 Promise 结束,无论成败,返回所有结果,且永远是 fulfilled 状态。

  5. **Promise.any 和 Promise.race 的区别?**答:race 取最先完成的(无论成败);any 只取最先成功的,失败的会忽略,直到有一个成功,全部失败才会 reject。


九、总结

  1. 4 种并发方法是 Promise 核心考点,也是项目中并行异步操作的必备工具;
  2. 重点掌握 each 方法的用法、区别和手写实现(all 和 race 是面试必写);
  3. 实战中根据需求选择对应方法:多接口需全部成功用 all,超时控制用 race,需所有结果用 allSettled,多源容错用 any;
  4. 理解每种方法的底层逻辑,能轻松应对面试手写和项目中的并发场景。
相关推荐
mfxcyh2 小时前
实现签名画板
前端·javascript·vue.js
是大强2 小时前
electron调用dll 方案
前端·javascript·electron
cd ~/Homestead2 小时前
Vue 配置跨域的两种方法
前端·javascript·vue.js
Moment2 小时前
AI 全栈时代,为什么推荐 NodeJs 服务端使用 NestJs
前端·javascript·后端
Moment2 小时前
AI全栈入门指南:什么是 NestJs
前端·javascript·后端
happymaker06262 小时前
Vue自定义指令、插槽、路由的简单使用
前端·javascript·vue.js
AIBox3652 小时前
codex api 配置教程:安装、鉴权、Windows 环境变量
javascript·人工智能·windows·gpt
斌味代码3 小时前
Next.js 14 App Router 完全指南:服务端组件、流式渲染与中间件实战
开发语言·javascript·中间件
~ rainbow~3 小时前
前端转型全栈(二)——NestJS 入门指南:从 Angular 开发者视角理解后端架构
前端·javascript·angular.js