一、核心需求与场景分析
当需要发送多个请求(如8个),但需限制同时并行的请求数量(如2个),核心目标是:
- 避免因并发请求过多导致浏览器性能下降或服务端压力过大;
- 保证请求按合理顺序处理,提升用户体验。
二、实现方案:基于Promise的并发控制
方案1:手动管理请求队列(ES6+原生实现)
通过维护待发送队列和进行中请求集合,逐步发送请求。
function sendRequests(urls) {
const maxConcurrent = 2; // 最大并发数
const results = []; // 存储所有请求结果
let pendingCount = 0; // 进行中请求数
const queue = [...urls]; // 待发送队列
// 发送请求的核心函数
function sendNext() {
// 若队列为空且无进行中请求,结束
if (queue.length === 0 && pendingCount === 0) {
return Promise.resolve(results);
}
// 当进行中请求数小于最大并发数时,发送新请求
while (pendingCount < maxConcurrent && queue.length > 0) {
pendingCount++;
const url = queue.shift();
results[urls.indexOf(url)] = fetch(url)
.then(res => res.json())
.then(data => {
console.log(`请求 ${url} 完成`);
return data;
})
.catch(error => {
console.error(`请求 ${url} 失败`, error);
return error;
})
.finally(() => {
pendingCount--; // 请求完成后减少计数
sendNext(); // 继续发送下一个请求
});
}
return Promise.all(results); // 返回所有请求结果
}
return sendNext();
}
// 使用示例
const urls = [
'https://api1.com', 'https://api2.com',
'https://api3.com', 'https://api4.com',
'https://api5.com', 'https://api6.com',
'https://api7.com', 'https://api8.com'
];
sendRequests(urls).then(results => {
console.log('所有请求完成', results);
});
方案2:使用async/await + 队列生成器(更简洁的ES7实现)
通过生成器函数管理请求队列,配合async/await处理异步逻辑。
async function sendRequests(urls, maxConcurrent = 2) {
const queue = [...urls];
const results = new Array(urls.length).fill(null);
let pending = 0;
// 生成器函数:逐个获取待发送的URL
function* createQueue() {
while (queue.length > 0) {
yield queue.shift();
}
}
const requestQueue = createQueue();
const sendRequest = async () => {
if (pending >= maxConcurrent) {
// 等待当前请求完成后再继续
await Promise.resolve();
}
const { value: url } = requestQueue.next();
if (url === undefined) return; // 队列为空时返回
pending++;
const index = urls.indexOf(url);
try {
const res = await fetch(url);
results[index] = await res.json();
console.log(`请求 ${url} 完成`);
} catch (error) {
results[index] = error;
console.error(`请求 ${url} 失败`, error);
} finally {
pending--;
await sendRequest(); // 递归发送下一个请求
}
};
// 启动多个并发请求
const starters = Array(maxConcurrent).fill().map(sendRequest);
return Promise.all(starters).then(() => results);
}
方案3:使用RxJS操作符(响应式编程实现)
通过RxJS的concatMap
和mergeMap
控制并发数。
import { from, of } from 'rxjs';
import { mergeMap, toArray } from 'rxjs/operators';
function sendRequests(urls, maxConcurrent = 2) {
return from(urls).pipe(
// mergeMap控制并发数,超过maxConcurrent的请求进入队列等待
mergeMap(url => fetch(url).then(res => res.json()), maxConcurrent),
toArray() // 收集所有结果
).toPromise();
}
方案4:封装并发控制工具函数(可复用)
将并发控制逻辑抽象为通用函数,支持任意异步任务。
/**
* 控制并发数量的通用函数
* @param tasks 异步任务数组(如() => fetch())
* @param maxConcurrent 最大并发数
*/
async function controlConcurrency(tasks, maxConcurrent) {
const results = new Array(tasks.length).fill(null);
let pending = 0;
let completed = 0;
async function runTask(index, task) {
pending++;
try {
results[index] = await task();
} catch (error) {
results[index] = error;
} finally {
pending--;
completed++;
// 发送下一个任务(如果有)
if (completed < tasks.length && pending < maxConcurrent) {
runTask(completed, tasks[completed]);
}
}
}
// 启动初始并发任务
for (let i = 0; i < Math.min(maxConcurrent, tasks.length); i++) {
runTask(i, tasks[i]);
}
// 等待所有任务完成
await new Promise(resolve => {
const interval = setInterval(() => {
if (completed === tasks.length) {
clearInterval(interval);
resolve(results);
}
}, 100);
});
return results;
}
// 使用示例
const urls = [
() => fetch('https://api1.com').then(res => res.json()),
() => fetch('https://api2.com').then(res => res.json()),
// ...其他请求函数
];
controlConcurrency(urls, 2).then(results => {
console.log('所有请求完成', results);
});
三、核心实现原理与关键步骤
- 请求队列管理:
- 将所有请求存入队列,按顺序等待发送。
- 并发数控制:
- 通过计数器
pendingCount
限制同时进行的请求数量。
- 通过计数器
- 请求调度:
- 当有请求完成时,从队列中取出下一个请求发送(递归或循环实现)。
- 结果收集:
- 确保结果按原始顺序返回(通过索引匹配)。
四、问题
1. 问:为什么需要控制请求并发数?
- 答:
- 浏览器对同一域名的并发请求数有限制(如Chrome默认6个),超出后请求会排队;
- 控制并发数可避免因请求过多导致的资源竞争、服务端过载或客户端性能下降。
2. 问:如何处理请求失败后的重试?
- 答:
- 在请求处理函数中添加重试逻辑(如最多重试3次):
async function fetchWithRetry(url, retries = 3) { for (let i = 0; i < retries; i++) { try { return await fetch(url); } catch (error) { if (i === retries - 1) throw error; console.log(`重试请求 ${url} (第${i+1}次)`); } } }
3. 问:请求并发控制与请求优先级如何结合?
- 答:
- 将请求按优先级排序(如重要接口优先),优先发送高优先级请求;
- 使用不同的并发数配置(如高优先级请求并发数为2,低优先级为1)。