在前端开发中,我们经常会遇到一个页面需要发起多个异步请求的场景 ------ 比如列表数据加载、多图片资源请求、批量接口调用等。如果放任这些请求同时发起,它们会相互竞争网络带宽,可能导致部分请求超时、页面加载卡顿,甚至影响用户体验。今天就来聊聊如何通过并发控制,让多个异步请求有序执行,避免带宽被单一请求霸占。
为什么需要控制并发?
想象一个场景:页面同时发起 6 个 AJAX 请求,其中一个请求需要 10 秒才能完成,其余请求分别需要 1-8 秒。如果不做任何控制,浏览器会同时发送这些请求,带宽会被长时间的请求占用,短请求也得排队,最终导致页面加载慢、用户体验差。
并发控制的核心思路是:限制同时运行的请求数量,将超出限制的请求放入队列等待,当有请求完成时,再从队列中取出下一个请求执行。
实现一个通用的并发控制器
接下来我们实现一个通用的并发控制类 Limit,支持自定义并发数,适配任意异步任务(比如 AJAX 请求、Promise 操作等)。
核心代码实现
javascript
// 模拟AJAX请求(实际场景可替换为真实接口调用)
function ajax(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 模拟请求超时/失败(时间>5000视为失败)
if (time > 5000) {
reject(new Error(`请求超时:${time}ms`));
} else {
resolve(`请求成功:${time}ms`);
}
}, time);
});
}
// 并发控制类
class Limit {
/**
* 构造函数
* @param {number} parallCount - 最大并发数,默认2
*/
constructor(parallCount = 2) {
this.tasks = []; // 待执行的任务队列
this.runningCount = 0; // 正在运行的任务数
this.parallCount = parallCount; // 最大并发数限制
}
/**
* 添加异步任务到队列
* @param {Function} task - 返回Promise的异步任务函数
* @returns {Promise} - 任务执行结果的Promise
*/
add(task) {
return new Promise((resolve, reject) => {
// 将任务和对应的resolve/reject存入队列
this.tasks.push({
task,
resolve,
reject
});
// 尝试执行任务(核心:添加任务后立即触发执行逻辑)
this._run();
});
}
/**
* 内部方法:执行队列中的任务
*/
_run() {
// 若当前运行任务数未达上限,且队列中有任务,则执行
if (this.runningCount < this.parallCount && this.tasks.length) {
// 取出队列第一个任务
const { task, resolve, reject } = this.tasks.shift();
this.runningCount++; // 运行任务数+1
// 执行异步任务
task()
.then(result => {
resolve(result); // 任务成功,传递结果
})
.catch(error => {
reject(error); // 任务失败,传递错误
})
.finally(() => {
// 任务完成(无论成功/失败),运行任务数-1,并继续执行下一个任务
this.runningCount--;
this._run();
});
}
}
}
使用示例
我们添加 6 个不同耗时的请求,限制最大并发数为 2,看看执行效果:
javascript
// 初始化并发控制器,限制同时运行2个请求
const limit = new Limit(2);
/**
* 封装添加任务的函数
* @param {number} time - 请求耗时
* @param {number} name - 任务名称(用于日志标识)
*/
function addTask(time, name) {
limit
.add(() => ajax(time)) // 传入返回Promise的任务函数
.then(result => {
console.log(`任务${name}完成:`, result);
})
.catch(error => {
console.log(`任务${name}失败:`, error.message);
});
}
// 添加6个测试任务
addTask(10000, 1); // 耗时10s(失败)
addTask(4000, 2); // 耗时4s(成功)
addTask(8000, 3); // 耗时8s(失败)
addTask(1000, 4); // 耗时1s(成功)
addTask(5000, 5); // 耗时5s(成功)
addTask(2000, 6); // 耗时2s(成功)
执行结果分析
由于并发数限制为 2,任务执行顺序如下:
-
初始执行任务 1(10s)和任务 2(4s);
-
4s 后任务 2 完成,立即执行任务 3(8s);
-
1s 后(总耗时 5s)任务 1 还在执行,任务 3 执行中,无空闲并发位;
-
5s 后任务 1 执行到 5s 时仍未完成,但 ajax 函数判定 > 5000ms 触发 reject,任务 1 失败,立即执行任务 4(1s);
-
任务 4 完成后执行任务 5(5s);
-
任务 3 执行 8s 后失败,执行任务 6(2s);
-
最终所有任务按 "并发数 2" 的规则有序执行,避免了带宽竞争。
核心逻辑解析
-
队列管理 :
tasks数组存放待执行的任务,每个任务包含异步函数、resolve 和 reject; -
并发数控制 :
runningCount实时记录正在运行的任务数,parallCount为最大并发数限制; -
自动执行 :每次调用
add添加任务后,立即触发\_run方法,尝试从队列中取出任务执行; -
任务收尾 :无论任务成功或失败,最终都会通过
finally减少运行数,并再次调用\_run,实现队列任务的自动衔接。
扩展与优化
这个基础版本的并发控制器可以根据实际需求扩展:
-
取消任务:添加取消队列中指定任务的方法;
-
优先级队列:支持按优先级执行任务,而非先进先出;
-
进度回调:添加全局进度回调,实时返回任务执行进度;
-
错误重试:对失败的任务支持自动重试,可配置重试次数;
-
批量添加:支持一次性添加多个任务,简化调用逻辑。
总结
前端并发请求控制是解决多请求带宽竞争的关键手段,通过限制并发数、队列管理的方式,让异步请求有序执行,既能保证网络资源的合理利用,也能提升页面加载的稳定性和用户体验。
本文实现的 Limit 类是一个通用的并发控制方案,不仅适用于 AJAX 请求,还能适配任何返回 Promise 的异步任务(比如文件上传、定时器操作等)。你可以根据自己的业务场景,基于这个基础版本扩展更多实用功能。