js请求的并发控制

JavaScript 前端并发请求控制方案总结

在前端开发中(如批量文件上传、大批量接口请求),为了防止浏览器卡顿或服务器压力过大,通常需要限制同一时刻的最大并发数(例如限制最多 10 个请求同时进行)。

本文总结了两种主流的实现方案:递归队列法Promise.race 竞速法


方案一:递归队列法 (Recursive Worker)

核心思维"收银台模式"。 假设有 10 个收银台(Worker),顾客(Tasks)排成一长队。收银台不关闭,处理完一个顾客后,立刻叫号处理下一位,直到队伍排空。

1. 实现原理

  1. 初始化 :根据最大并发数 max,一次性启动 max 个异步函数(Worker)。
  2. 状态维护 :维护一个全局索引 index,指向任务列表中下一个待处理的任务。
  3. 自动流转:Worker 完成当前任务后,通过递归调用自己,去领取并执行下一个任务。
  4. 结束条件 :当 index 超出任务总数时,Worker 停止递归。

2. 代码示例

javascript 复制代码
/**
 * @param {Function[]} tasks 任务数组 (返回 Promise 的函数)
 * @param {number} max 最大并发数
 */
function concurrentRun(tasks, max = 10) {
  const results = [];
  let index = 0; // 全局指针

  // 递归执行器
  async function worker() {
    // 递归出口:任务取完了
    if (index >= tasks.length) return;

    // 1. 占位:先保存当前索引,然后指针后移
    const i = index; 
    index++; 

    try {
      // 2. 执行:运行任务并保存结果
      // console.log(`开始任务 ${i}`);
      const res = await tasks[i]();
      results[i] = res; // 按索引保存,保证结果顺序
    } catch (err) {
      results[i] = err; // 捕获错误,防止中断
    } finally {
      // 3. 接力:无论成功失败,立马递归领取下一个
      await worker();
    }
  }

  // 4. 启动:同时开启 max 个并发线程
  const workers = [];
  const runCount = Math.min(tasks.length, max); // 防止任务数少于并发数
  
  for (let i = 0; i < runCount; i++) {
    workers.push(worker());
  }

  // 5. 等待:所有 worker 都收工了,整体才算完成
  return Promise.all(workers).then(() => results);
}

3. 优缺点

  • 优点:逻辑清晰,稳定性高,天然保证结果顺序(Result 数组按索引存储)。
  • 缺点:需要定义辅助函数,代码量稍多。
  • 推荐指数:⭐⭐⭐⭐⭐ (工程落地与面试首选)

方案二:Promise.race 竞速法 (Dynamic Pool)

核心思维:"停车场模式"。

停车场只有 10 个车位。车一辆接一辆来,只要有空位就进。如果满了,门口的栏杆就放下,直到有一辆车出来(Promise.race),才放下一辆车进去。

1. 实现原理

  1. 遍历:使用循环遍历所有任务。
  2. 入列 :将任务包装后推入一个"正在执行数组" (executing)。
  3. 包装 :每个任务完成后,必须执行 splice 操作将自己从 executing 中移除。
  4. 阻塞 :判断 executing.length >= max。如果满了,使用 await Promise.race(executing) 阻塞主线程,等待最快的一个任务完成腾出坑位。

2. 代码示例

JavaScript

javascript 复制代码
/**
 * @param {Function[]} tasks 任务数组
 * @param {number} max 最大并发数
 */
async function limitRequest(tasks, max = 10) {
  const results = [];
  const executing = []; // 正在执行的任务队列

  // 使用 entries() 拿到索引,为了保证结果顺序
  for (const [index, task] of tasks.entries()) {
    
    // 1. 创建任务:执行并存储结果
    const p = task().then(res => results[index] = res);

    // 2. 包装任务:任务完成后,从 executing 队列中移除自己
    // 关键:e 必须引用 p.then 的返回值,确保 race 等待的是"删除操作"完成
    const e = p.then(() => executing.splice(executing.indexOf(e), 1));
    
    // 3. 入列
    executing.push(e);

    // 4. 竞速:如果队列满了,等待最快的一个执行完
    if (executing.length >= max) {
      await Promise.race(executing);
    }
  }

  // 5. 收尾:等待剩余的任务完成
  await Promise.all(executing);
  return results;
}

3. 优缺点

  • 优点 :利用 async/await 线性逻辑,代码看起来较精简。
  • 缺点:涉及微任务时序问题(必须正确包装 Promise),逻辑稍显绕弯,容易写出 Bug。
  • 推荐指数:⭐⭐⭐⭐

总结对比

维度 递归队列法 (方案一) Promise.race 竞速法 (方案二)
并发维持机制 总量守恒:走一个,递归补一个 阻塞等待 :满了就 await race 暂停循环
代码结构 闭包 + 递归函数 For 循环 + 动态数组
执行视角 开启 N 个永久的"线程" 动态维护一个"线程池"
稳定性 高,容错率好 中,需注意 splice 的时序
适用场景 通用业务、面试手写 个人项目、脚本工具

附:测试用例代码

可以将上述任意一种方案配合以下代码进行测试:

JavaScript

javascript 复制代码
// 模拟请求:返回一个 Promise,耗时 100~1000ms
const mockRequest = (id) => {
  return () => new Promise((resolve) => {
    const time = Math.random() * 1000 + 100;
    console.log(`🚀 任务 ${id} 开始`);
    setTimeout(() => {
      console.log(`✅ 任务 ${id} 完成 (耗时 ${Math.floor(time)}ms)`);
      resolve(`结果 ${id}`);
    }, time);
  });
};

// 生成 20 个任务
const tasks = Array.from({ length: 20 }, (_, i) => mockRequest(i));

// 执行测试 (并发数限制为 3)
console.time('总耗时');
concurrentRun(tasks, 3).then(res => {
  console.log('--- 所有任务结束 ---');
  console.log(res);
  console.timeEnd('总耗时');
});
相关推荐
我命由我123455 小时前
CesiumJS 笔记 - 获取容器中心点、Cartesian3 clone 方法、修改 Cartesian3 对象的高度
前端·javascript·css·前端框架·html·html5·js
Hopebearer_6 小时前
页面突然只剩 DOM?一次静态资源版本错配排查
前端·部署
东风破_6 小时前
ESLint 是什么?为什么你的项目需要它?
前端·后端·代码规范
圣殿骑士-Khtangc8 小时前
Go字符串高效拼接性能对比与底层原理分析
服务器·前端·golang
BigTopOne9 小时前
【ijkplayer】 硬解码流程
前端
kyriewen9 小时前
DeepSeek Harness开源第一天我就上手了——和Claude Code的差距比想象中大
前端·ai编程·deepseek
捡田螺的小男孩9 小时前
什么是 Skill?手把手带你写一个简单有用的 Skill!
前端·后端·程序员
IT_陈寒9 小时前
Redis集群这个坑,差点让我通宵
前端·人工智能·后端
90后的晨仔10 小时前
uni-app 路由跳转与页面导航 —— 终极详解(对标 iOS / Android / 鸿蒙三端原生)
前端
Heo10 小时前
大厂前端调试不能只会debugger
前端·javascript·面试