
文章目录 6
手写带并发限制的 Promise 调度器
核心思路
用一个计数器追踪正在执行的任务数,用一个等待队列存放还没轮到的任务。每当有任务完成,就从队列里取下一个任务执行。
实现
class Scheduler {
constructor(max) {
this.max = max; // 最大并发数
this.running = 0; // 当前正在执行的任务数
this.queue = []; // 等待队列
}
add(task) {
return new Promise((resolve, reject) => {
// 把任务包装一下,带上 resolve/reject
const job = () => {
task().then(resolve, reject).finally(() => {
this.running--;
this._runNext();
});
};
if (this.running < this.max) {
// 没达到并发上限,直接执行
this.running++;
job();
} else {
// 超过上限,放入等待队列
this.queue.push(job);
}
});
}
_runNext() {
if (this.queue.length > 0) {
this.running++;
const job = this.queue.shift();
job();
}
}
}使用示例
const scheduler = new Scheduler(2); // 最多同时跑 2 个
function timeout(id, ms) {
return () => new Promise(resolve => {
console.log(`任务 ${id} 开始`);
setTimeout(() => {
console.log(`任务 ${id} 完成`);
resolve(id);
}, ms);
});
}
scheduler.add(timeout('A', 3000)); // 立即执行
scheduler.add(timeout('B', 1000)); // 立即执行(并发 2)
scheduler.add(timeout('C', 2000)); // 等 A 或 B 完成后执行
scheduler.add(timeout('D', 500)); // 继续排队
// 输出:
// 任务 A 开始
// 任务 B 开始
// 任务 B 完成 (B 先完成,C 开始)
// 任务 C 开始
// 任务 A 完成 (A 完成,D 开始)
// 任务 D 开始
// 任务 C 完成
// 任务 D 完成实际应用场景
1. 图片批量上传
// 用户一次性选了 50 张图片,同时上传最多 3 个
const uploadScheduler = new Scheduler(3);
const results = await Promise.all(
files.map(file => uploadScheduler.add(() => uploadFile(file)))
);如果不限制并发,浏览器会同时发出 50 个请求,直接打满带宽,还可能触发服务端限流。
2. 爬虫/API 批量请求
// 爬取 B 站关注列表的 200 个 UP 主动态
const crawler = new Scheduler(3); // 最多 3 个并发
const dynamics = await Promise.all(
uids.map(uid => crawler.add(() => fetchUserDynamics(uid)))
);不限制并发的话,几百个请求瞬间发出,会被反爬风控(-412)封禁。
3. 页面图片懒加载的请求控制
// 首屏同时加载 30 张图片,限制同时最多 4 个
const imgLoader = new Scheduler(4);
images.forEach(img => {
imgLoader.add(() => loadImage(img.dataset.src));
});4. 单元测试并行执行控制
// 100 个测试用例,限制最多 5 个并行
const testRunner = new Scheduler(5);
const results = await Promise.all(
testCases.map(tc => testRunner.add(() => runTest(tc)))
);共同本质
这些场景的核心问题是相同的:
一次性抛出的任务太多,但系统(浏览器、服务器、带宽)承受能力有限,需要排队+限流。
不定并发数的 Promise.all 是"有多少同时来多少",加上调度器后就变成了"最多同时跑 n 个,其余排队等待"。
相关文章
- 面试题-基础
Vue 3 响应式解构:为什么替换 user 后出现“李四 / 王五”
通过一道 Vue 3 代码题,区分 reactive 对象解构、嵌套属性修改与对象替换的行为,并用 toRef 保持源属性关联。文章保留首次作答及追问补答,说明从误判到纠正的过程。
- 面试题-基础
Vue 3 编辑弹窗:草稿状态、取消重开与父子数据契约
以一个始终挂载的姓名编辑弹窗为场景,给出覆盖数据归属、保存取消、重开初始化和外部更新冲突的完整 Vue 3 解法。结合首次回答与递进补答,说明本地草稿、脏标记和版本校验各自解决的问题。
- 面试题-基础
JavaScript 浅拷贝、深拷贝与 structuredClone:前端状态更新的正确方式
通过一道对象复制代码题,梳理 JavaScript 浅拷贝、JSON 深拷贝、structuredClone 和 transferable object 的边界,并解释 React 结构共享与 Vue 3 响应式更新的差异。
评论
0 条评论会在审核后展示,邮箱不会公开。