前言
在前端开发的世界里,异步任务就像一群急性子的“选手”,总想一拥而上,争先恐后地完成比赛。但如果不加以控制,网络请求、文件上传、数据处理等任务就会像春运抢票一样,场面失控,资源争抢,甚至导致性能瓶颈和用户体验下降。于是,并发控制机制应运而生,成为前端工程师的“调度大师”。
为什么需要并发控制?
想象一下,如果你让所有异步任务同时发起请求,服务器可能会被“挤爆”,浏览器也会卡成PPT。并发控制的核心目的就是:限制同时进行的任务数量,让资源分配更合理,避免“拥堵”和“踩踏”。
- 场景举例:
- 批量图片上传,限制同时上传的数量,防止带宽被瞬间占满。
- 爬虫抓取网页,控制并发数,避免被目标网站封禁。
- 批量数据处理,合理分配CPU资源,提升整体性能。
并发控制的原理
并发控制机制本质上就是一个“任务调度器”,它负责:
- 维护一个任务队列,所有待执行的任务都排队等候。
- 设定最大并发数,只允许有限数量的任务同时进行。
- 每当有任务完成,立即从队列中取下一个任务补位,保证“流水线”不断。
就像电影院检票口,只允许有限人数同时进入,后面的人只能乖乖排队。
代码实现大揭秘
下面我们结合实际代码,详细拆解一个并发控制类的实现过程。
1. 任务模拟器 ajax
首先,我们用一个 ajax 函数模拟异步任务:
// 模拟一个异步任务,传入不同的耗时参数 time
function ajax(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 如果任务耗时超过 5000ms,则判定为失败
if (time > 5000) {
reject(time)
}
// 否则判定为成功
resolve(time)
}, time) // 任务实际耗时由 time 决定
})
}
讲解:
- 这个函数就是我们的“任务工厂”,每次调用都会返回一个 Promise。
- 用
setTimeout模拟异步操作,time 越大任务越慢。 - 超过 5 秒的任务会被 reject,模拟失败场景。
2. 并发控制类 Limit
核心来了!我们用一个 Limit 类来实现并发控制:
// 并发控制类,负责调度任务队列
class Limit {
constructor(parallCount = 2) {
this.parallCount = parallCount // 最大并发数,默认 2
this.tasks = [] // 任务队列,存储所有待执行任务
this.runningCount = 0 // 当前正在运行的任务数量
}
// 添加新任务,返回一个 Promise 以便外部获取结果
add(task) {
return new Promise((resolve, reject) => {
// 将任务及其对应的 resolve/reject 存入队列
this.tasks.push({
task, // 任务函数,需返回 Promise
resolve, // 任务成功时调用
reject, // 任务失败时调用
})
// 每次添加任务都尝试调度执行
this._run()
})
}
// 内部调度器,负责按最大并发数执行任务
_run() {
// 如果当前运行任务数未达到最大并发,且队列有待执行任务
if (this.runningCount < this.parallCount && this.tasks.length) {
// 从队列取出一个任务
const {task, resolve, reject }= this.tasks.shift()
this.runningCount++ // 当前运行任务数加一
// 执行任务(需返回 Promise)
task().then((res) => {
resolve(res) // 任务成功,通知外部
this.runningCount-- // 运行任务数减一
this._run() // 递归调度下一个任务
}).catch((res) => {
this.runningCount-- // 运行任务数减一
reject(res) // 任务失败,通知外部
this._run() // 递归调度下一个任务
})
}
}
}
详细讲解:
parallCount控制最大并发数,灵活应对不同场景。tasks是一个先进先出的队列,保证任务顺序执行。runningCount实时记录当前正在执行的任务数量。add方法将任务包装成对象,方便后续调度和结果回调。_run方法是调度核心,每次有任务完成都会递归调用,保证队列持续流动。- 这种设计可以防止任务“堆积”,让资源利用率最大化。
3. 添加任务并观察效果
我们用 addTask 函数批量添加任务,并观察并发控制的效果:
// 封装添加任务的函数,方便批量测试
function addTask(time, name) {
limit
.add(() => ajax(time)) // 添加任务,传入异步函数
.then((res) => {
// 任务成功时输出结果
console.log(`任务${name}完成`, res);
})
.catch((res) => {
// 任务失败时输出结果
console.log(`任务${name}失败`, res);
})
}
// 批量添加不同耗时的任务
addTask(10000, '1') // 10秒,必失败
addTask(4000, '2') // 4秒,成功
addTask(2000, '3') // 2秒,成功
addTask(5000, '4') // 5秒,成功
addTask(1000, '5') // 1秒,成功
addTask(7000, '6') // 7秒,必失败
讲解:
- 每个任务耗时不同,模拟真实场景下的异步请求。
- 并发数设置为 2,意味着最多同时跑 2 个任务。
- 任务完成或失败后,自动补位下一个任务,队列像流水线一样高效运转。
4. 运行流程可视化与分析
假如你用表格或时序图展示,任务的执行顺序会像这样:
| 时间(ms) | 任务1 | 任务2 | 任务3 | 任务4 | 任务5 | 任务6 |
|---|---|---|---|---|---|---|
| 0 | 开始 | 开始 | 等待 | 等待 | 等待 | 等待 |
| 4000 | 运行 | 完成 | 开始 | 等待 | 等待 | 等待 |
| 5000 | 运行 | 完成 | 开始 | 等待 | 等待 | |
| 10000 | 完成 | 完成 | 开始 | 等待 | ||
| ... | 完成 | 开始 | ||||
| ... | 完成 |
分析补充:
- 任务队列始终保持“满载”,只要有空位就自动补上新任务。
- 这种机制极大提升了资源利用率,避免了“空闲浪费”。
- 失败的任务不会影响队列流动,健壮性更强。
代码优化与扩展建议
- 支持动态调整并发数:可以增加方法动态修改
parallCount,适应不同场景。 - 任务优先级调度:可扩展为优先队列,让高优先级任务优先执行。
- 失败重试机制:为失败的任务增加重试次数,提高健壮性。
- 任务取消功能:支持任务取消,提升灵活性。
- 进度回调与事件监听:可增加事件机制,实时反馈任务进度。
- Promise.all 并发对比:传统的
Promise.all会一次性并发所有任务,容易造成资源拥堵,而并发控制机制则能“限流”,更适合大规模异步场景。
实战场景应用
- 批量文件上传:限制同时上传的文件数量,防止网络拥堵。
- 爬虫抓取:控制并发数,避免被目标网站封禁。
- 数据处理:合理分配 CPU 资源,提升整体性能。
- 前端渲染:异步渲染大数据列表,避免页面卡顿。
- API 请求限流:防止接口被频繁调用导致封禁。
总结
并发控制机制是前端开发中的“交通警察”,让异步任务有序排队,资源分配更合理,性能更高效。无论是面试还是实际项目,掌握并发控制都是前端工程师的必备技能。希望本文能帮你轻松理解并发控制的原理和实现,写出更高效、更优雅的前端代码!