前端实现控制并发请求的队列

Hello!这里是一个练习时长两年半的前端开发工程师。

今天遇到了一个问题:在进行web开发时,通常会遇到需要向服务器端发送多个网络请求的情况。然而,服务器通常会对并发请求的数量有所限制,过多的并发请求可能会导致服务器负载过重,甚至导致请求失败。那么,如何在前端控制并发请求的数量?

看到这个问题之后,我学习了两种在前端实现控制并发请求的策略,记录一下:

  1. 基于Promise和递归的方法,
  2. 利用async/await语法和类的封装来实现。

首先这里是一个网络请求:

function sendRequest(url) {
  return fetch(url).then(response => response.json());
}

那么第一种方法是使用Promise.all结合递归函数来控制并发的请求数量。通过维护两个队列:一个是正在执行的请求队列(activeQueue),另一个是等待执行的请求队列(waitingQueue)。当activeQueue的长度小于最大并发数时,就将请求从waitingQueue移动到activeQueue并执行。该方法的关键在于通过Promise的状态管理和递归调用来控制请求的并发执行。

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类:

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类来管理并发的请求:

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的事件循环和异步编程模型
  1. 事件循环(Event Loop) : JavaScript是一种单线程语言,它依赖事件循环来处理异步操作。事件循环允许JavaScript代码执行时不会被阻塞,即便是在执行长时间运行的操作(如网络请求)时也是如此。事件循环通过任务队列(Task Queues)和微任务队列(Microtask Queues)来管理异步事件,确保这些事件按正确的顺序执行。
  2. 非阻塞I/O: 这是实现异步编程的重要特性,允许在等待某些操作(如文件读写、网络请求等)完成时,程序可以继续执行其他任务,而不是停下来等待。
Promise 和 async/await 的工作原理
  1. Promise: 是一个代表未来完成或失败的异步操作的对象。Promise有三种状态:pending(待定),fulfilled(已实现),rejected(已拒绝)。通过使用Promise对象,开发者可以用链式调用的方式组织回调,并通过.then().catch().finally()方法来处理异步操作的成功、失败或完成。
  2. async/await: 是建立在Promises之上的一个更高层次的抽象。async关键字使得函数自动返回一个Promise,await关键字使得在Promise完成前暂停函数的执行,并等待Promise解析。这种语法让异步代码看起来和写同步代码一样容易,同时保持非阻塞的特性。

不管是使用Promise还是async/await,控制并发请求的核心原理都涉及到以下几点:

  1. 任务分配: 将大量的异步任务分成若干批次,每批次包含固定数量的任务,以避免一次性发送过多请求给服务器。
  2. 队列管理: 维护一个或多个队列来控制任务的执行。当正在执行的任务数量达到设定的上限时,新的任务将放入等待队列中。一旦有任务完成,便从等待队列中取出下一个任务执行。
  3. 使用异步控制流: 利用JavaScript的事件循环和异步特性,可以在不阻塞主线程的情况下等待任务的完成,并根据完成的状态来决定后续行动。

最后呢,总结一下:控制前端的并发请求对于提高应用的性能和用户体验至关重要。通过实现请求队列,可以有效地管理并发请求,避免服务器过载,并确保请求的高效处理。无论是选择基于Promise的方法,还是采用更现代的async/await语法,重要的是理解其背后的原理,并根据实际需求选择最合适的实现方式。

原文链接:https://juejin.cn/post/7349052422059294756 作者:SD_Kai

(0)
上一篇 2024年3月23日 下午4:44
下一篇 2024年3月23日 下午4:54

相关推荐

发表回复

登录后才能评论