Hello!这里是一个练习时长两年半的前端开发工程师。
今天遇到了一个问题:在进行web开发时,通常会遇到需要向服务器端发送多个网络请求的情况。然而,服务器通常会对并发请求的数量有所限制,过多的并发请求可能会导致服务器负载过重,甚至导致请求失败。那么,如何在前端控制并发请求的数量?
看到这个问题之后,我学习了两种在前端实现控制并发请求的策略,记录一下:
- 基于Promise和递归的方法,
- 利用
async/await
语法和类的封装来实现。
首先这里是一个网络请求:
js
function sendRequest(url) {
return fetch(url).then(response => response.json());
}
那么第一种方法是使用Promise.all结合递归函数来控制并发的请求数量。通过维护两个队列:一个是正在执行的请求队列(activeQueue
),另一个是等待执行的请求队列(waitingQueue
)。当activeQueue
的长度小于最大并发数时,就将请求从waitingQueue
移动到activeQueue
并执行。该方法的关键在于通过Promise
的状态管理和递归调用来控制请求的并发执行。
js
const MAX_CONCURRENT_REQUESTS = 10;
let activeQueue = [];
let waitingQueue = [];
function enqueueRequest(request) {
return new Promise((resolve, reject) => {
// 启动函数,以便于控制执行时机
const startRequest = () => {
sendRequest(request)
.then(resolve)
.catch(reject)
.finally(() => {
// 请求完成后,从活动队列移除
activeQueue = activeQueue.filter(item => item !== startRequest);
// 尝试启动下一个等待中的请求
if (waitingQueue.length > 0) {
const nextRequest = waitingQueue.shift();
activeQueue.push(nextRequest);
nextRequest();
}
});
};
if (activeQueue.length < MAX_CONCURRENT_REQUESTS) {
// 如果活动队列未满,直接加入活动队列并执行
activeQueue.push(startRequest);
startRequest();
} else {
// 活动队列已满,加入等待队列
waitingQueue.push(startRequest);
}
});
}
// 示例:
let urls = []; // 假设这是你需要请求的URL数组
for (let i = 0; i < 20; i++) {
urls.push(`https://example.com/api/data?id=${i}`);
}
Promise.all(urls.map(url => enqueueRequest(url)))
.then(results => {
console.log("所有请求均已完成", results);
}).catch(error => {
console.error("请求中有错误", error);
});
第二种方法是创建一个AsyncQueue
类,利用async/await
来实现相同的功能。AsyncQueue
类通过维护一个计数器和一个等待执行的任务队列,控制同时运行的任务数量。在达到最大并发数时,新的任务会等待,直到有任务完成并释放了占用的并发槽位。这种方法使代码更加简洁,易于理解和维护。
先创建一个AsyncQueue
类:
js
class AsyncQueue {
constructor(maxConcurrent) {
this.maxConcurrent = maxConcurrent;
this.currentRunning = 0;
this.queue = [];
}
// 将请求封装成异步任务并加入队列
async enqueue(task) {
if (this.currentRunning >= this.maxConcurrent) {
// 如果当前运行的任务已达上限,则将任务加入队列等待
await new Promise(resolve => this.queue.push(resolve));
}
return this.run(task);
}
// 运行任务
async run(task) {
this.currentRunning++;
const result = await task();
this.currentRunning--;
// 完成一个任务后,从队列中取出并执行下一个任务(如果有的话)
if (this.queue.length > 0) {
const next = this.queue.shift();
next();
}
return result;
}
}
然后使用这个AsyncQueue
类来管理并发的请求:
js
const MAX_CONCURRENT_REQUESTS = 10;
const requestQueue = new AsyncQueue(MAX_CONCURRENT_REQUESTS);
// 示例:使用AsyncQueue进行网络请求
async function performRequests(urls) {
// 包装sendRequest函数,使其变成无参数的函数传入enqueue
const tasks = urls.map(url => () => sendRequest(url));
const results = [];
for (let task of tasks) {
// 等待每个请求完成并收集结果
const result = await requestQueue.enqueue(task);
results.push(result);
}
return results;
}
// 假设这是你需要请求的URL数组
const urls = [];
for (let i = 0; i < 20; i++) {
urls.push(`https://example.com/api/data?id=${i}`);
}
performRequests(urls).then(results => {
console.log("所有请求均已完成", results);
}).catch(error => {
console.error("在执行请求时发生错误", error);
});
基于Promise方法和async/await
语法来控制并发网络请求时,虽然表面上看起来是不同的编程技巧,但其背后的核心原理都是基于JavaScript的事件循环(Event Loop)和异步编程模型:
JavaScript的事件循环和异步编程模型
- 事件循环(Event Loop) : JavaScript是一种单线程语言,它依赖事件循环来处理异步操作。事件循环允许JavaScript代码执行时不会被阻塞,即便是在执行长时间运行的操作(如网络请求)时也是如此。事件循环通过任务队列(Task Queues)和微任务队列(Microtask Queues)来管理异步事件,确保这些事件按正确的顺序执行。
- 非阻塞I/O: 这是实现异步编程的重要特性,允许在等待某些操作(如文件读写、网络请求等)完成时,程序可以继续执行其他任务,而不是停下来等待。
Promise 和 async/await 的工作原理
- Promise : 是一个代表未来完成或失败的异步操作的对象。Promise有三种状态:pending(待定),fulfilled(已实现),rejected(已拒绝)。通过使用Promise对象,开发者可以用链式调用的方式组织回调,并通过
.then()
,.catch()
和.finally()
方法来处理异步操作的成功、失败或完成。 - async/await : 是建立在Promises之上的一个更高层次的抽象。
async
关键字使得函数自动返回一个Promise,await
关键字使得在Promise完成前暂停函数的执行,并等待Promise解析。这种语法让异步代码看起来和写同步代码一样容易,同时保持非阻塞的特性。
不管是使用Promise还是async/await
,控制并发请求的核心原理都涉及到以下几点:
- 任务分配: 将大量的异步任务分成若干批次,每批次包含固定数量的任务,以避免一次性发送过多请求给服务器。
- 队列管理: 维护一个或多个队列来控制任务的执行。当正在执行的任务数量达到设定的上限时,新的任务将放入等待队列中。一旦有任务完成,便从等待队列中取出下一个任务执行。
- 使用异步控制流: 利用JavaScript的事件循环和异步特性,可以在不阻塞主线程的情况下等待任务的完成,并根据完成的状态来决定后续行动。
最后呢,总结一下:控制前端的并发请求对于提高应用的性能和用户体验至关重要。通过实现请求队列,可以有效地管理并发请求,避免服务器过载,并确保请求的高效处理。无论是选择基于Promise的方法,还是采用更现代的async/await
语法,重要的是理解其背后的原理,并根据实际需求选择最合适的实现方式。