有这样一些场景:
- 页面一加载,需要同时发 10 个请求,结果页面卡住,服务器也快崩了。
- 用户可以批量操作,一次点击触发了几十个上传文件的请求,浏览器直接转圈圈。
当后端处理不过来时,前端一股脑地把请求全发过去,只会让情况更糟。
核心思想就一句话:不要一次性把所有请求都发出去,让它们排队,一个一个来,或者一小批一小批来。
这就好比超市结账,只有一个收银台,却来了100个顾客。最好的办法就是让他们排队,而不是一拥而上。我们的“请求队列”就是这个“排队管理员”。
直接上代码:一个即插即用的请求队列
不用复杂的分析,直接复制下面的 RequestPool
类到我们的项目里。它非常小巧,只有不到 40 行代码。
/*** 一个简单的请求池/请求队列,用于控制并发* @example* const pool = new RequestPool(3); // 限制并发数为 3* pool.add(() => myFetch('/api/1'));* pool.add(() => myFetch('/api/2'));*/
class RequestPool {/*** @param {number} limit - 并发限制数*/constructor(limit = 3) {this.limit = limit; // 并发限制数this.queue = []; // 等待的请求队列this.running = 0; // 当前正在运行的请求数}/*** 添加一个请求到池中* @param {Function} requestFn - 一个返回 Promise 的函数* @returns {Promise}*/add(requestFn) {return new Promise((resolve, reject) => {this.queue.push({ requestFn, resolve, reject });this._run(); // 每次添加后,都尝试运行});}_run() {// 只有当 正在运行的请求数 < 限制数 且 队列中有等待的请求时,才执行while (this.running < this.limit && this.queue.length > 0) {const { requestFn, resolve, reject } = this.queue.shift(); // 取出队首的任务this.running++;requestFn().then(resolve).catch(reject).finally(() => {this.running--; // 请求完成,空出一个位置this._run(); // 尝试运行下一个});}}
}
如何使用?三步搞定!
假设你有一个请求函数 mockApi
,它会模拟一个比较慢的接口
// 1.模拟一个慢的接口
function MockApi(id: number) {const delay = Math.random() * 1000 + 500;return new Promise((resolve) => {setTimeout(() => {console.log(`[${id}] 请求完成`);resolve(`任务${id}的结果`);}, delay);});
}
// 2. 创建一个请求池,限制并发为 2const pool = new RequesetPool(2);
// 3. 把你的请求扔进去
for (let i = 0; i < 10; i++) {pool.add(() => MockApi(i)).then((result: any) => console.log(`[${i}] 收到的结果:${result}`));
}
发生了什么?
当你运行上面的代码,你会看到:
[1]
和[2]
的请求几乎同时开始。[3]
、[4]
、[5]
、[6]
在乖乖排队。- 当
[1]
或[2]
中任意一个完成后,队列中的[3]
马上就会开始。 - 整个过程,同时运行的请求数永远不会超过 2 个。
控制台输出类似这样:
发生了什么?
当你运行上面的代码,你会看到:
[1]
和[2]
的请求几乎同时开始。[3]
、[4]
、[5]
、[6]
在乖乖排队。- 当
[1]
或[2]
中任意一个完成后,队列中的[3]
马上就会开始。- 整个过程,同时运行的请求数永远不会超过 2 个。
控制台输出类似这样: