You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用普通Promise实现支持限频API调用的异步队列?

实现带速率限制的异步队列(基于原生Promise)

要实现这个每秒最多10次调用的异步队列,核心是要控制任务的执行时机,而不是让Promise一创建就立刻运行。你之前的代码有两个关键问题:一是直接存储Promise会导致任务立即执行,无法限速;二是没有给add方法返回一个可供外部监听结果的Promise。下面是完整的实现方案:

核心思路

  1. 存储任务生成函数而非直接存储Promise:这样我们可以在合适的时机再执行任务,避免任务提前启动。
  2. 跟踪时间窗口内的调用次数:每秒重置一次计数,确保不超过速率限制。
  3. 每个add调用返回一个Promise:让外部代码能监听任务的成功/失败结果。

完整TypeScript实现

export class AsyncQueue {
  // 存储待执行的任务函数(函数返回Promise)
  private taskQueue: Array<{
    task: () => Promise<any>;
    resolve: (value: any) => void;
    reject: (reason?: any) => void;
  }> = [];
  // 每秒允许的最大调用次数
  private readonly rateLimit: number;
  // 当前时间窗口的起始时间(毫秒)
  private windowStartTime: number = Date.now();
  // 当前时间窗口内已完成的调用次数
  private windowCount: number = 0;

  constructor(rateLimit: number = 10) {
    this.rateLimit = rateLimit;
  }

  /**
   * 添加一个异步任务到队列
   * @param task 返回Promise的任务函数
   * @returns 该任务执行结果的Promise
   */
  add(task: () => Promise<any>): Promise<any> {
    return new Promise((resolve, reject) => {
      // 将任务和对应的resolve/reject存入队列
      this.taskQueue.push({ task, resolve, reject });
      // 尝试处理队列中的下一个任务
      this.processQueue();
    });
  }

  private async processQueue(): Promise<void> {
    if (this.taskQueue.length === 0) return;

    const now = Date.now();
    // 检查是否已经过了当前时间窗口(1秒)
    if (now - this.windowStartTime >= 1000) {
      // 重置时间窗口
      this.windowStartTime = now;
      this.windowCount = 0;
    }

    // 如果当前窗口还有调用额度,执行任务
    if (this.windowCount < this.rateLimit) {
      const { task, resolve, reject } = this.taskQueue.shift()!;
      this.windowCount++;

      try {
        const result = await task();
        resolve(result);
      } catch (err) {
        reject(err);
      } finally {
        // 任务完成后,继续处理下一个任务
        this.processQueue();
      }
    } else {
      // 当前窗口额度用完,等待到下一秒再处理
      const waitTime = 1000 - (now - this.windowStartTime);
      setTimeout(() => this.processQueue(), waitTime);
    }
  }

  get length(): number {
    return this.taskQueue.length;
  }
}

使用示例

// 初始化队列,设置每秒最多10次调用
const queue = new AsyncQueue(10);

// 模拟API调用函数
const mockApi = {
  get: (path: string) => {
    console.log(`调用API: ${path}`);
    return Promise.resolve(`来自${path}的响应`);
  }
};

// 添加多个任务到队列
for (let i = 0; i < 25; i++) {
  queue.add(() => mockApi.get(`/data/${i}`))
    .then(res => console.log(res))
    .catch(err => console.error(err));
}

关键细节解释

  • 任务函数存储:我们存的是() => Promise<any>而不是直接的Promise,这样任务只会在processQueue调用时才执行,完全由队列控制执行时机。
  • 时间窗口控制:每次处理任务前,先检查当前是否在1秒窗口内,如果超出就重置计数;如果窗口内还有额度,就执行任务,否则等待到下一秒。
  • 结果传递:add方法返回的Promise会和任务的resolve/reject绑定,外部代码可以通过.then()/.catch()拿到任务的结果或错误。
  • 自动调度:每次添加任务或完成任务后,都会触发processQueue,确保队列能持续处理任务。

扩展建议

  • 如果需要并发数限制(比如同时最多运行5个任务),可以修改processQueue逻辑,用activeCount跟踪当前运行的任务数,而不是时间窗口计数。
  • 可以添加clear()方法清空队列,或者pause()/resume()方法控制队列的运行状态。

内容的提问来源于stack exchange,提问作者KarimS

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 09:59:30