在现代前端开发中,我们经常需要处理大量异步请求,但浏览器对同一域名的并发请求数有限制(通常6-8个)。为了避免性能问题,我们需要手动控制并发请求数。本文将介绍如何使用 Promise.all
和 Promise.race
来实现精确的并发控制,确保同时只发送3个请求。
一、为什么需要限制并发请求?
- 避免浏览器请求阻塞:浏览器对同一域名有并发请求限制
- 减轻服务器压力:防止瞬时大量请求压垮服务器
- 优化用户体验:有序的请求队列比混乱的大量请求更可控
- 资源合理利用:平衡网络带宽和CPU使用率
二、核心实现原理
我们将使用以下两个Promise API组合实现并发控制:
Promise.all
:等待所有Promise完成Promise.race
:等待任意一个Promise完成
基本思路是:
- 初始化一个执行中的Promise集合
- 每当有新的请求时,如果已达到并发上限,就等待
Promise.race
- 当一个请求完成时,从集合中移除,腾出空间给新请求
- 最终使用
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);
});
五、代码解析
-
任务队列管理:
executing
Set集合用于跟踪正在执行的Promise- 通过
executing.size
实时获取当前并发数
-
并发控制逻辑:
- 当
executing.size >= limit
时,使用Promise.race(executing)
等待任意一个任务完成 - 任务完成后会自动从
executing
中移除,腾出并发空间
- 当
-
结果收集:
- 所有Promise都被推入
results
数组 - 最终使用
Promise.all(results)
等待所有任务完成
- 所有Promise都被推入
-
错误处理:
- 每个Promise都添加了catch处理,确保出错时也能从
executing
中移除 - 错误会通过
Promise.all
的catch传递出来
- 每个Promise都添加了catch处理,确保出错时也能从
六、高级应用场景
-
文件分片上传:
async function uploadFiles(files, limit = 3) { const tasks = files.map(file => () => uploadChunk(file)); return runWithConcurrency(tasks, limit); }
-
批量API请求:
async function fetchMultipleApis(apiUrls, limit = 3) { const tasks = apiUrls.map(url => () => fetch(url)); return runWithConcurrency(tasks, limit); }
-
数据库操作:
async function batchInsert(records, limit = 3) { const tasks = records.map(record => () => db.insert(record)); return runWithConcurrency(tasks, limit); }
七、性能优化建议
-
动态调整并发数:
// 根据网络状况动态调整 const limit = navigator.connection?.effectiveType === '4g' ? 5 : 3;
-
优先级队列:
// 实现优先级任务调度 tasks.sort((a, b) => b.priority - a.priority);
-
断点续传:
// 记录已完成任务,异常恢复后跳过 const completed = new Set(); const tasks = remainingTasks.filter(task => !completed.has(task.id));
八、总结
通过结合Promise.all
和Promise.race
,我们可以实现优雅的并发请求控制。这种方法具有以下优点:
- 代码简洁:不到20行核心逻辑
- 功能强大:支持错误处理、结果收集
- 通用性好:适用于各种异步场景
- 性能优异:精确控制资源使用
这种模式已经成为现代前端开发中处理并发请求的标准实践,掌握它能够显著提升应用的稳定性和用户体验。