如何用普通Promise实现支持限频API调用的异步队列?
实现带速率限制的异步队列(基于原生Promise)
要实现这个每秒最多10次调用的异步队列,核心是要控制任务的执行时机,而不是让Promise一创建就立刻运行。你之前的代码有两个关键问题:一是直接存储Promise会导致任务立即执行,无法限速;二是没有给add方法返回一个可供外部监听结果的Promise。下面是完整的实现方案:
核心思路
- 存储任务生成函数而非直接存储Promise:这样我们可以在合适的时机再执行任务,避免任务提前启动。
- 跟踪时间窗口内的调用次数:每秒重置一次计数,确保不超过速率限制。
- 每个
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
相关产品推荐
相关产品推荐

