使用 Promise.all 与 Promise.race 实现并发请求限制(3个并发)

20 阅读3分钟

在现代前端开发中,我们经常需要处理大量异步请求,但浏览器对同一域名的并发请求数有限制(通常6-8个)。为了避免性能问题,我们需要手动控制并发请求数。本文将介绍如何使用 Promise.allPromise.race 来实现精确的并发控制,确保同时只发送3个请求。

一、为什么需要限制并发请求?

  1. 避免浏览器请求阻塞:浏览器对同一域名有并发请求限制
  2. 减轻服务器压力:防止瞬时大量请求压垮服务器
  3. 优化用户体验:有序的请求队列比混乱的大量请求更可控
  4. 资源合理利用:平衡网络带宽和CPU使用率

二、核心实现原理

我们将使用以下两个Promise API组合实现并发控制:

  • Promise.all:等待所有Promise完成
  • Promise.race:等待任意一个Promise完成

基本思路是:

  1. 初始化一个执行中的Promise集合
  2. 每当有新的请求时,如果已达到并发上限,就等待Promise.race
  3. 当一个请求完成时,从集合中移除,腾出空间给新请求
  4. 最终使用Promise.all等待所有请求完成

三、完整实现代码

/**
 * 限制并发数量的异步任务执行器
 * @param {Array<Function>} tasks 返回Promise的任务数组
 * @param {number} limit 并发限制数
 * @returns {Promise<Array>} 所有任务结果的数组
 */
async function runWithConcurrency(tasks, limit = 3) {
  // 存储所有任务的Promise
  const results = [];
  // 使用Set来追踪正在执行的任务
  const executing = new Set();
  
  for (const task of tasks) {
    // 如果达到并发限制,等待任意一个任务完成
    if (executing.size >= limit) {
      await Promise.race(executing);
    }
    
    // 创建并执行新任务
    const p = task()
      .then((res) => {
        executing.delete(p); // 任务完成,从执行集合中移除
        return res;
      })
      .catch((err) => {
        executing.delete(p); // 即使失败也要移除
        throw err;
      });
    
    executing.add(p); // 添加到执行集合
    results.push(p);  // 存储到结果数组
  }
  
  // 等待所有任务完成
  return Promise.all(results);
}

四、使用示例

// 模拟异步请求函数
function mockRequest(id, delay) {
  return new Promise((resolve) => {
    console.log(`请求 ${id} 开始`);
    setTimeout(() => {
      console.log(`请求 ${id} 完成`);
      resolve(`结果 ${id}`);
    }, delay);
  });
}

// 创建10个请求任务
const tasks = [];
for (let i = 1; i <= 10; i++) {
  tasks.push(() => mockRequest(i, Math.random() * 2000));
}

// 执行并发控制
runWithConcurrency(tasks, 3)
  .then((results) => {
    console.log('所有请求完成:', results);
  })
  .catch((err) => {
    console.error('请求出错:', err);
  });

五、代码解析

  1. 任务队列管理

    • executing Set集合用于跟踪正在执行的Promise
    • 通过executing.size实时获取当前并发数
  2. 并发控制逻辑

    • executing.size >= limit时,使用Promise.race(executing)等待任意一个任务完成
    • 任务完成后会自动从executing中移除,腾出并发空间
  3. 结果收集

    • 所有Promise都被推入results数组
    • 最终使用Promise.all(results)等待所有任务完成
  4. 错误处理

    • 每个Promise都添加了catch处理,确保出错时也能从executing中移除
    • 错误会通过Promise.all的catch传递出来

六、高级应用场景

  1. 文件分片上传

    async function uploadFiles(files, limit = 3) {
      const tasks = files.map(file => () => uploadChunk(file));
      return runWithConcurrency(tasks, limit);
    }
    
  2. 批量API请求

    async function fetchMultipleApis(apiUrls, limit = 3) {
      const tasks = apiUrls.map(url => () => fetch(url));
      return runWithConcurrency(tasks, limit);
    }
    
  3. 数据库操作

    async function batchInsert(records, limit = 3) {
      const tasks = records.map(record => () => db.insert(record));
      return runWithConcurrency(tasks, limit);
    }
    

七、性能优化建议

  1. 动态调整并发数

    // 根据网络状况动态调整
    const limit = navigator.connection?.effectiveType === '4g' ? 5 : 3;
    
  2. 优先级队列

    // 实现优先级任务调度
    tasks.sort((a, b) => b.priority - a.priority);
    
  3. 断点续传

    // 记录已完成任务,异常恢复后跳过
    const completed = new Set();
    const tasks = remainingTasks.filter(task => !completed.has(task.id));
    

八、总结

通过结合Promise.allPromise.race,我们可以实现优雅的并发请求控制。这种方法具有以下优点:

  1. 代码简洁:不到20行核心逻辑
  2. 功能强大:支持错误处理、结果收集
  3. 通用性好:适用于各种异步场景
  4. 性能优异:精确控制资源使用

这种模式已经成为现代前端开发中处理并发请求的标准实践,掌握它能够显著提升应用的稳定性和用户体验。