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('总耗时');
});
相关推荐
是你的小橘呀1 小时前
从 "渣男" 到 "深情男":Promise 如何让 JS 变得代码变得专一又靠谱
前端·javascript·html
baozj1 小时前
告别截断与卡顿:我的前端PDF导出优化实践
前端·javascript·vue.js
傲文博一1 小时前
为什么我的产品尽量不用「外置」动态链接库
前端·后端
Healer9181 小时前
Promise限制重复请求
前端
梵得儿SHI1 小时前
Vue 响应式原理深度解析:Vue2 vs Vue3 核心差异 + ref/reactive 实战指南
前端·javascript·vue.js·proxy·vue响应式系统原理·ref与reactive·vue响应式实践方案
chenyunjie1 小时前
我做了一个编辑国际化i18n json文件的命令行工具
前端
Emma歌小白1 小时前
移除视觉对象里“行的型号”造成的行级筛选,但不移除用户的 slicer 筛选
前端
茶杯6751 小时前
“舒欣双免“方案助力MSI-H/dMMR结肠癌治疗新突破
java·服务器·前端
昔人'1 小时前
css `svh` 单位
前端·css