后端接口太慢,前端如何优雅地实现一个“请求队列”,避免并发打爆服务器?


有这样一些场景:

  • 页面一加载,需要同时发 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. [1] [2] 的请求几乎同时开始。
  2. [3] [4] [5] [6] 在乖乖排队。
  3. [1] [2] 中任意一个完成后,队列中的 [3] 马上就会开始。
  4. 整个过程,同时运行的请求数 ** 永远不会超过 2 个 ** 。

** 控制台输出类似这样: **

[1] 🚀 请求开始...  
[2] 🚀 请求开始...  
// (此时 3, 4, 5, 6 在排队)  
  
[1] ✅ 请求完成!  
[1] 收到结果: 任务 1 的结果  
[3] 🚀 请求开始...  // 1号完成,3号立刻补上  
  
[2] ✅ 请求完成!  
[2] 收到结果: 任务 2 的结果  
[4] 🚀 请求开始...  // 2号完成,4号立刻补上  
  
...  

它是如何工作的?

  1. **add(requestFn) ** : 你扔给它的不是一个已经开始的请求,而是一个“启动器”函数 () => mockApi(i) 。它把这个“启动器”放进 queue 数组里排队。
  2. **_run() ** : 这是管理员。它会检查:
    • 现在有空位吗?( running < limit
    • 有人在排队吗?( queue.length > 0
    • 如果两个条件都满足,就从队首叫一个号( queue.shift() ),让它开始工作(执行 requestFn() ),并且把正在工作的计数 running 加一。
  3. **.finally() ** : 这是最关键的一步。每个请求不管是成功还是失败,最后都会执行 finally 里的代码。它会告诉管理员:“我完事了!”,然后把 running 减一,并再次呼叫管理员 _run() 来看看能不能让下一个人进来。

这样就形成了一个完美的自动化流程: ** 完成一个,就自动启动下一个 ** 。

以后再遇到需要批量发请求的场景,别再用 Promise.all 一股脑全发出去了。

把上面那段小小的 RequestPool 代码复制到你的项目里,用它来包裹我们的请求函数。只需要设置一个合理的并发数(比如 2 或
3),就能在不修改后端代码的情况下,大大减轻服务器的压力,让我们的应用运行得更平稳。

这是一种简单、优雅且非常有效的前端优化手段。