前言
想象一下,你打开一个页面,6 个请求同时冲向服务器,就像 6 辆车挤在单行道上 ------ 有的爆胎(超时失败) ,有的龟速(加载缓慢),用户看着转圈的加载动画,心态直接崩了。
一、为什么要给请求 "限速"?
上了这么多年网,想必大家应该都经历过以下画面:
- 打开电商详情页,一堆商品图片、评论、推荐接口同时发起,页面半天加载不出来
- 某个大文件上传时,其他小请求直接超时失败
- 浏览器控制台里一片红色的
net::ERR_CONNECTION_RESET
这本质上就是并发请求过多,带宽被 "挤爆" ,就像早高峰的地铁,大家都想上车,结果谁也动不了。
前端控制并发,就是给这些请求当 "交通警察":
- 规定同一时间最多只能有 N 辆车(请求)在路上
- 剩下的车乖乖在停车场(任务队列)排队
- 等路上的车到站(请求完成),再放一辆进去
这样既能避免服务器和带宽过载,又能保证所有请求最终都能被处理。
二、先造个 "模拟请求":让请求有快有慢
我们先写一个模拟的异步请求函数 ajax,用 setTimeout 模拟网络延迟,超过 5000ms 就直接 "请求失败":
JavaScript
function ajax(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if(time > 5000) {
reject('请求失败')
} else {
resolve('请求成功')
}
}, time)
})
}
三、核心工具:并发控制器 Limit 类
接下来就是我们的主角 ------并发控制器 Limit,它会帮我们管理所有请求的 "上路权限":
JavaScript
class Limit {
constructor(parallCount = 2) {
this.tasks = []; // 任务队列:存还没上路的请求
this.runningCount = 0; // 计数器:当前正在跑的请求数
this.parallCount = parallCount; // 最大并发数:默认最多2个请求同时跑
}
// 添加任务到队列
add(task) {
return new Promise((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(resolve, reject)
.finally(() => {
this.runningCount--; // 任务完成/失败,计数器-1
this._run(); // 继续调度下一个任务
});
}
}
}
代码拆解:
- 构造函数 :默认最大并发数
2,你可以改成 3、5 甚至 10,看业务需求 - add 方法:把请求包装成任务塞进队列,然后立刻尝试启动调度
- _run 方法 (私有调度器):
- 检查:当前正在跑的请求数 < 最大并发数,且队列里还有任务
- 满足条件:从队列里取第一个任务,计数器 + 1,执行任务
- 任务结束(无论成功 / 失败):计数器 - 1,再次调用
_run放新任务上路
四、实战演示:给 6 个请求 "限流"
我们创建一个并发控制器,然后一次性加 6 个任务,看看它们是怎么排队执行的:
JavaScript
// 初始化控制器:最多同时 2 个请求
let limit = new Limit(2);
// 封装添加任务的函数
function addTask(time, index) {
limit
.add(() => ajax(time))
.then(() => {
console.log(`任务${index}完成`);
})
.catch(() => {
console.log(`任务${index}失败`);
});
}
// 一次性添加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 → 成功
执行流程(脑补一下):
- 0s 时 :任务 1、任务 2 先上路,
runningCount = 2,剩下任务 3-6 在队列排队 - 4s 时:任务 2 完成,计数器变回 1 → 立刻放任务 3 上路,现在跑任务 1、3
- 8s 时:任务 1 失败,计数器变回 1 → 放任务 4 上路,现在跑任务 3、4
- 9s 时:任务 4 完成,计数器变回 1 → 放任务 5 上路,现在跑任务 3、5
- 10s 时:任务 3 失败,计数器变回 1 → 放任务 6 上路,现在跑任务 5、6
- 11s 时:任务 5 完成,计数器变回 1 → 队列空了,任务 6 继续跑
- 12s 时:任务 6 完成,所有任务结束

你看,全程最多只有 2 个请求在同时执行,既不会挤爆带宽,又保证了所有请求都被处理。
结语:并发控制不是 "慢",是 "稳"
很多人会问:"我同时发 6 个请求不是更快吗?"
其实不然 ------ 就像你同时开 10 个下载任务,每个都慢得像蜗牛,不如限制成 2 个,每个都跑满带宽,整体完成时间反而更短,用户也不会看到满屏的失败提示。
前端并发控制,本质上是一种 "以退为进" 的智慧:
- 不追求 "瞬间全发" 的冲动,而是追求 "有序高效" 的稳定
- 给服务器、带宽、浏览器都留足呼吸空间
- 最终换来的是:页面加载更快、失败率更低、用户体验更好