后端接口太慢,前端如何优雅地实现一个“请求队列”,避免并发打爆服务器?(一次性发送多个请求的优化方案)
【前端请求队列优化方案】针对批量请求导致页面卡顿和服务器过载的问题,本文提出了请求队列的解决方案。通过实现一个RequestPool类,可控制请求并发数量(默认3个),支持失败重试(可配置次数和间隔)、超时处理(默认20秒)和请求去重功能。该方案采用队列机制管理请求,确保同时运行的请求数不超过设定阈值,并自动处理任务排队和接续。使用示例显示,相比Promise.all的无序并发,该方案能有效减轻服
有这样一些场景:
- 页面一加载,需要同时发 10+ 个请求,结果页面卡住,服务器也快崩了。
- 用户可以批量操作,一次点击触发了几十个上传文件的请求,浏览器直接转圈圈。
当后端处理不过来时,前端一股脑地把请求全发过去,只会让情况更糟。
核心思想就一句话:不要一次性把所有请求都发出去,让它们排队,一个一个来,或者一小批一小批来。
这就好比超市结账,只有一个收银台,却来了100个顾客。最好的办法就是让他们排队,而不是一拥而上。我们的“请求队列”就是这个“排队管理员”。
直接上代码:一个即插即用的请求队列(基础)
不用复杂的分析,直接复制下面的 RequestPool
类到我们的项目里。
/**
* 一个完整优雅的请求池/请求队列,用于控制并发、失败重试次数
* @example
* const pool = new RequestPool(3); // 限制并发数为 3
* pool.add(() => myFetch('/api/1'));
* pool.add(() => myFetch('/api/2'));
*/
class RequestPool {
constructor(limit = 3) {
this.limit = limit; // 并发限制数
this.queue = []; // 等待的请求队列
this.running = 0; // 当前正在运行的请求数
}
/**
* 添加一个请求到池中
* @param {Function} requestFn - 一个返回 Promise 的函数
* @param {number} [retryCount=1] - 失败重试次数,默认为1
* @param {number} [retryInterval=500] - 重试间隔(毫秒),默认为500ms
* @param {number} [timeout=20000] - 请求超时时间(毫秒),默认为20000ms
* @returns {Promise}
*/
add(requestFn, retryCount = 1, retryInterval = 500, timeout = 20000) {
if (typeof requestFn !== 'function') {
throw new TypeError('requestFn must be a function');
}
return new Promise((resolve, reject) => {
this.queue.push({ requestFn, resolve, reject, retryCount, retryInterval, timeout });
this._run(); // 每次添加任务后,尝试运行队列中的任务
});
}
_run() {
if (this.running >= this.limit || this.queue.length === 0) {
return; // 如果达到并发限制或队列为空,直接退出
}
const { requestFn, resolve, reject, retryCount, retryInterval, timeout } = this.queue.shift();
this.running++;
this._executeRequest(requestFn, resolve, reject, retryCount, retryInterval, timeout);
}
/**
* 执行请求,并处理重试逻辑
* @param {Function} requestFn - 请求函数
* @param {Function} resolve - 成功回调
* @param {Function} reject - 失败回调
* @param {number} retryCount - 剩余重试次数
* @param {number} retryInterval - 重试间隔(毫秒)
* @param {number} timeout - 请求超时时间(毫秒)
*/
_executeRequest(requestFn, resolve, reject, retryCount, retryInterval, timeout) {
let currentRetry = 0;
const execute = () => {
const requestPromise = requestFn();
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('Request timed out'));
}, timeout);
});
Promise.race([requestPromise, timeoutPromise])
.then(resolve)
.catch((error) => {
if (currentRetry < retryCount) {
console.warn(`[RequestPool] 任务失败,${currentRetry + 1}次重试中...`);
currentRetry++;
setTimeout(execute, retryInterval); // 重试间隔后再次执行
} else {
console.error(`[RequestPool] 任务失败,重试次数用完:`, error);
reject(error);
}
})
.finally(() => {
this.running--;
this._run(); // 继续处理下一个任务
});
};
execute(); // 开始执行请求
}
clear() {
this.queue = []; // 清空队列
this.running = 0; // 重置运行中的请求数
console.log('[RequestPool] 请求池已清空');
}
}
如何使用?三步搞定!
假设你有一个请求函数 mockApi
,它会模拟一个比较慢的接口。
// 1.模拟接口,随机失败
function mockApi(id) {
const delay = Math.random() * 1500 + 2000; // 随机延迟 2 ~ 3.5 秒
const shouldFail = Math.random() < 0.5; // 50% 的概率失败
console.log(`[${id}] 请求开始...`);
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
console.log(`[${id}] 请求失败,将重试`);
reject(new Error(`任务${id}失败`));
} else {
console.log(`[${id}] 请求完成!`);
resolve(`任务:${id}的结果`);
}
}, delay);
});
}
// 2.创建请求池,限制并发为 2
const pool = new RequestPool(2);
// 3.添加任务,每个任务失败时重试一次,重试间隔为 1000ms,超时时间为 5000ms
for (let i = 1; i <= 5; i++) {
pool.add(() => mockApi(i), 2, 1000, 5000)
.then(result => {
console.log(`[${i}] 收到结果: ${result}`);
})
.catch(error => {
console.error(`[${i}] 最终失败: ${error.message}`);
});
}
发生了什么?
当你运行上面的代码,你会看到:
[1]
和[2]
的请求几乎同时开始。[3]
、[4]
、[5]
、[6]
在乖乖排队。- 当
[1]
或[2]
中任意一个完成后,队列中的[3]
马上就会开始。 - 整个过程,同时运行的请求数永远不会超过 2 个。
- 当出现 请求失败 后,会尝试再请求一次。
控制台输出类似这样:
如何避免重复请求? 共享已存在的结果?(推荐)
class RequestPool {
constructor(limit = 3, { log = true } = {}) {
this.limit = limit; // 并发限制数
this.queue = []; // 等待的请求队列
this.running = 0; // 当前正在运行的请求数
this.activeRequests = new Map(); // 用于记录正在执行的请求
this.log = log; // 是否输出日志
}
/**
* 生成请求的唯一标识符
* @param {Function} requestFn - 请求函数
* @param {any} [identifier] - 请求的唯一标识符(可选)
* @returns {string}
*/
_generateRequestId(requestFn, identifier) {
// 如果提供了标识符,则直接使用
if (identifier !== undefined) {
return JSON.stringify(identifier);
}
// 否则,使用函数的 toString() 方法
return requestFn.toString();
}
/**
* 添加一个请求到池中
* @param {Function} requestFn - 一个返回 Promise 的函数
* @param {any} [identifier] - 请求的唯一标识符(可选)
* @param {number} [retryCount=1] - 失败重试次数,默认为1
* @param {number} [retryInterval=500] - 重试间隔(毫秒),默认为500ms
* @param {number} [timeout=20000] - 请求超时时间(毫秒),默认为20000ms
* @returns {Promise}
*/
add(requestFn, identifier, retryCount = 1, retryInterval = 500, timeout = 20000) {
if (typeof requestFn !== 'function') {
throw new TypeError('requestFn must be a function');
}
const requestId = this._generateRequestId(requestFn, identifier);
// 检查是否已经存在相同的请求
if (this.activeRequests.has(requestId)) {
if (this.log) {
console.warn(`[RequestPool] 重复请求已存在,共享结果: ${requestId}`);
}
return this.activeRequests.get(requestId); // 返回已存在的请求的 Promise
}
const requestPromise = new Promise((resolve, reject) => {
this.queue.push({ requestFn, resolve, reject, retryCount, retryInterval, timeout });
this._run(); // 每次添加任务后,尝试运行队列中的任务
});
// 将请求的 Promise 存储到 activeRequests 中
this.activeRequests.set(requestId, requestPromise);
// 当请求完成时,从 activeRequests 中移除
requestPromise.finally(() => {
this.activeRequests.delete(requestId);
});
return requestPromise;
}
_run() {
if (this.running >= this.limit || this.queue.length === 0) {
return; // 如果达到并发限制或队列为空,直接退出
}
const { requestFn, resolve, reject, retryCount, retryInterval, timeout } = this.queue.shift();
this.running++;
this._executeRequest(requestFn, resolve, reject, retryCount, retryInterval, timeout);
}
/**
* 执行请求,并处理重试逻辑
* @param {Function} requestFn - 请求函数
* @param {Function} resolve - 成功回调
* @param {Function} reject - 失败回调
* @param {number} retryCount - 剩余重试次数
* @param {number} retryInterval - 重试间隔(毫秒)
* @param {number} timeout - 请求超时时间(毫秒)
*/
_executeRequest(requestFn, resolve, reject, retryCount, retryInterval, timeout) {
let currentRetry = 0;
const execute = () => {
const requestPromise = requestFn();
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('Request timed out'));
}, timeout);
});
Promise.race([requestPromise, timeoutPromise])
.then(resolve)
.catch((error) => {
if (currentRetry < retryCount) {
if (this.log) {
console.warn(`[RequestPool] 任务失败,${currentRetry + 1}次重试中...`);
}
currentRetry++;
setTimeout(execute, retryInterval); // 重试间隔后再次执行
} else {
if (this.log) {
console.error(`[RequestPool] 任务失败,重试次数用完:`, error);
}
reject(error);
}
})
.finally(() => {
this.running--;
this._run(); // 继续处理下一个任务
});
};
execute(); // 开始执行请求
}
clear() {
this.queue = []; // 清空队列
this.running = 0; // 重置运行中的请求数
this.activeRequests.clear(); // 清空正在执行的请求
if (this.log) {
console.log('[RequestPool] 请求池已清空');
}
}
/**
* 取消一个正在进行的请求
* @param {any} identifier - 请求的唯一标识符
*/
cancel(identifier) {
const requestId = this._generateRequestId(() => {}, identifier);
if (this.activeRequests.has(requestId)) {
const requestPromise = this.activeRequests.get(requestId);
if (requestPromise.cancel) {
requestPromise.cancel(); // 调用取消方法
}
this.activeRequests.delete(requestId);
if (this.log) {
console.log(`[RequestPool] 请求已取消: ${requestId}`);
}
} else {
if (this.log) {
console.warn(`[RequestPool] 未找到请求: ${requestId}`);
}
}
}
}
// 模拟接口,随机失败
function mockApi(id) {
const delay = Math.random() * 1500 + 2000; // 随机延迟 2 ~ 3.5 秒
const shouldFail = Math.random() < 0.5; // 50% 的概率失败
console.log(`[${id}] 请求开始...`);
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
console.log(`[${id}] 请求失败,将重试`);
reject(new Error(`任务${id}失败`));
} else {
console.log(`[${id}] 请求完成!`);
resolve(`任务:${id}的结果`);
}
}, delay);
});
}
// 创建请求池,限制并发为 2
const pool = new RequestPool(2, { log: true });
// 添加任务,每个任务失败时重试一次,重试间隔为 1000ms,超时时间为 5000ms
for (let i = 1; i <= 5; i++) {
pool.add(() => mockApi(i), i, 2, 1000, 5000)
.then(result => {
console.log(`[${i}] 收到结果: ${result}`);
})
.catch(error => {
console.error(`[${i}] 最终失败: ${error.message}`);
});
}
// 添加重复请求
pool.add(() => mockApi(1), 1, 2, 1000, 5000)
.then(result => {
console.log(`[重复请求1] 收到结果: ${result}`);
})
.catch(error => {
console.error(`[重复请求1] 最终失败: ${error.message}`);
});
它是如何工作的?(简洁)
/**
* 一个简单的请求池/请求队列,用于控制并发
* @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(); // 尝试运行下一个
});
}
}
}
// 1.模拟二个慢接口
function mockApi(id) {
const delay = Math.random() * 1500 + 2000;// 随机延迟 2 ~ 3.5 秒
console.log(`[${id}]岁 请求开始.··`);
return new Promise(resolve => {
setTimeout(() => {
console.log(`[${id}] 请求完成!`);
resolve(`任务:${id}};的结果`);
}, delay);
});
}
// 2.创建一个请求池,限制并发为 2
const pool = new RequestPool(2);
// 3.把你的请求扔进去
for (let i = 1; i <= 6; i++) {
pool.add(() => mockApi(i)).then(result => {
console.log(`[${i}]收到结果:${result}`);
});
}
add(requestFn)
: 你扔给它的不是一个已经开始的请求,而是一个“启动器”函数() => mockApi(i)
。它把这个“启动器”放进queue
数组里排队。_run()
: 这是管理员。它会检查:- 现在有空位吗?(
running < limit
) - 有人在排队吗?(
queue.length > 0
) - 如果两个条件都满足,就从队首叫一个号(
queue.shift()
),让它开始工作(执行requestFn()
),并且把正在工作的计数running
加一。
- 现在有空位吗?(
.finally()
: 这是最关键的一步。每个请求不管是成功还是失败,最后都会执行finally
里的代码。它会告诉管理员:“我完事了!”,然后把running
减一,并再次呼叫管理员_run()
来看看能不能让下一个人进来。
这样就形成了一个完美的自动化流程:完成一个,就自动启动下一个。
以后再遇到需要批量发请求的场景,别再用 Promise.all
一股脑全发出去了。Promise.all的弊端:
如果并发数过多,可能会超过服务器的最大连接数限制或者导致客户端的资源耗尽(如内存溢出)
把上面那段小小的 RequestPool
代码复制到你的项目里,用它来包裹我们的请求函数。只需要设置一个合理的并发数(比如 2 或 3),就能在不修改后端代码的情况下,大大减轻服务器的压力,让我们的应用运行得更平稳。
这是一种简单、优雅且非常有效的前端优化手段。
更多推荐
所有评论(0)