有这样一些场景:

  • 页面一加载,需要同时发 10+ 个请求,结果页面卡住,服务器也快崩了。
  • 用户可以批量操作,一次点击触发了几十个上传文件的请求,浏览器直接转圈圈。

当后端处理不过来时,前端一股脑地把请求全发过去,只会让情况更糟。

核心思想就一句话:不要一次性把所有请求都发出去,让它们排队,一个一个来,或者一小批一小批来。

这就好比超市结账,只有一个收银台,却来了100个顾客。最好的办法就是让他们排队,而不是一拥而上。我们的“请求队列”就是这个“排队管理员”。

直接上代码:一个即插即用的请求队列(基础)

不用复杂的分析,直接复制下面的 RequestPool 类到我们的项目里。

/**
 * 一个完整优雅的请求池/请求队列,用于控制并发、失败重试次数
 * @example
 * const pool = new RequestPool(3); // 限制并发数为 3
 * pool.add(() => myFetch('/api/1'));
 * pool.add(() => myFetch('/api/2'));
 */
class RequestPool {
    constructor(limit = 3) {
        this.limit = limit; // 并发限制数
        this.queue = [];    // 等待的请求队列
        this.running = 0;   // 当前正在运行的请求数
    }

    /**
     * 添加一个请求到池中
     * @param {Function} requestFn - 一个返回 Promise 的函数
     * @param {number} [retryCount=1] - 失败重试次数,默认为1
     * @param {number} [retryInterval=500] - 重试间隔(毫秒),默认为500ms
     * @param {number} [timeout=20000] - 请求超时时间(毫秒),默认为20000ms
     * @returns {Promise}
     */
    add(requestFn, retryCount = 1, retryInterval = 500, timeout = 20000) {
        if (typeof requestFn !== 'function') {
            throw new TypeError('requestFn must be a function');
        }

        return new Promise((resolve, reject) => {
            this.queue.push({ requestFn, resolve, reject, retryCount, retryInterval, timeout });
            this._run(); // 每次添加任务后,尝试运行队列中的任务
        });
    }

    _run() {
        if (this.running >= this.limit || this.queue.length === 0) {
            return; // 如果达到并发限制或队列为空,直接退出
        }

        const { requestFn, resolve, reject, retryCount, retryInterval, timeout } = this.queue.shift();
        this.running++;

        this._executeRequest(requestFn, resolve, reject, retryCount, retryInterval, timeout);
    }

    /**
     * 执行请求,并处理重试逻辑
     * @param {Function} requestFn - 请求函数
     * @param {Function} resolve - 成功回调
     * @param {Function} reject - 失败回调
     * @param {number} retryCount - 剩余重试次数
     * @param {number} retryInterval - 重试间隔(毫秒)
     * @param {number} timeout - 请求超时时间(毫秒)
     */
    _executeRequest(requestFn, resolve, reject, retryCount, retryInterval, timeout) {
        let currentRetry = 0;

        const execute = () => {
            const requestPromise = requestFn();
            const timeoutPromise = new Promise((_, reject) => {
                setTimeout(() => {
                    reject(new Error('Request timed out'));
                }, timeout);
            });

            Promise.race([requestPromise, timeoutPromise])
                .then(resolve)
                .catch((error) => {
                    if (currentRetry < retryCount) {
                        console.warn(`[RequestPool] 任务失败,${currentRetry + 1}次重试中...`);
                        currentRetry++;
                        setTimeout(execute, retryInterval); // 重试间隔后再次执行
                    } else {
                        console.error(`[RequestPool] 任务失败,重试次数用完:`, error);
                        reject(error);
                    }
                })
                .finally(() => {
                    this.running--;
                    this._run(); // 继续处理下一个任务
                });
        };

        execute(); // 开始执行请求
    }

    clear() {
        this.queue = []; // 清空队列
        this.running = 0; // 重置运行中的请求数
        console.log('[RequestPool] 请求池已清空');
    }
}

如何使用?三步搞定!

假设你有一个请求函数 mockApi,它会模拟一个比较慢的接口。

// 1.模拟接口,随机失败
function mockApi(id) {
    const delay = Math.random() * 1500 + 2000; // 随机延迟 2 ~ 3.5 秒
    const shouldFail = Math.random() < 0.5; // 50% 的概率失败
    console.log(`[${id}] 请求开始...`);
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (shouldFail) {
                console.log(`[${id}] 请求失败,将重试`);
                reject(new Error(`任务${id}失败`));
            } else {
                console.log(`[${id}] 请求完成!`);
                resolve(`任务:${id}的结果`);
            }
        }, delay);
    });
}

// 2.创建请求池,限制并发为 2
const pool = new RequestPool(2);

// 3.添加任务,每个任务失败时重试一次,重试间隔为 1000ms,超时时间为 5000ms
for (let i = 1; i <= 5; i++) {
    pool.add(() => mockApi(i), 2, 1000, 5000)
        .then(result => {
            console.log(`[${i}] 收到结果: ${result}`);
        })
        .catch(error => {
            console.error(`[${i}] 最终失败: ${error.message}`);
        });
}

发生了什么?

当你运行上面的代码,你会看到:

  1. [1] 和 [2] 的请求几乎同时开始。
  2. [3][4][5][6] 在乖乖排队。
  3. 当 [1] 或 [2] 中任意一个完成后,队列中的 [3] 马上就会开始。
  4. 整个过程,同时运行的请求数永远不会超过 2 个
  5. 当出现 请求失败 后,会尝试再请求一次。

控制台输出类似这样:

如何避免重复请求? 共享已存在的结果?(推荐

class RequestPool {
    constructor(limit = 3, { log = true } = {}) {
        this.limit = limit; // 并发限制数
        this.queue = [];    // 等待的请求队列
        this.running = 0;   // 当前正在运行的请求数
        this.activeRequests = new Map(); // 用于记录正在执行的请求
        this.log = log;     // 是否输出日志
    }

    /**
     * 生成请求的唯一标识符
     * @param {Function} requestFn - 请求函数
     * @param {any} [identifier] - 请求的唯一标识符(可选)
     * @returns {string}
     */
    _generateRequestId(requestFn, identifier) {
        // 如果提供了标识符,则直接使用
        if (identifier !== undefined) {
            return JSON.stringify(identifier);
        }
        // 否则,使用函数的 toString() 方法
        return requestFn.toString();
    }

    /**
     * 添加一个请求到池中
     * @param {Function} requestFn - 一个返回 Promise 的函数
     * @param {any} [identifier] - 请求的唯一标识符(可选)
     * @param {number} [retryCount=1] - 失败重试次数,默认为1
     * @param {number} [retryInterval=500] - 重试间隔(毫秒),默认为500ms
     * @param {number} [timeout=20000] - 请求超时时间(毫秒),默认为20000ms
     * @returns {Promise}
     */
    add(requestFn, identifier, retryCount = 1, retryInterval = 500, timeout = 20000) {
        if (typeof requestFn !== 'function') {
            throw new TypeError('requestFn must be a function');
        }

        const requestId = this._generateRequestId(requestFn, identifier);

        // 检查是否已经存在相同的请求
        if (this.activeRequests.has(requestId)) {
            if (this.log) {
                console.warn(`[RequestPool] 重复请求已存在,共享结果: ${requestId}`);
            }
            return this.activeRequests.get(requestId); // 返回已存在的请求的 Promise
        }

        const requestPromise = new Promise((resolve, reject) => {
            this.queue.push({ requestFn, resolve, reject, retryCount, retryInterval, timeout });
            this._run(); // 每次添加任务后,尝试运行队列中的任务
        });

        // 将请求的 Promise 存储到 activeRequests 中
        this.activeRequests.set(requestId, requestPromise);

        // 当请求完成时,从 activeRequests 中移除
        requestPromise.finally(() => {
            this.activeRequests.delete(requestId);
        });

        return requestPromise;
    }

    _run() {
        if (this.running >= this.limit || this.queue.length === 0) {
            return; // 如果达到并发限制或队列为空,直接退出
        }

        const { requestFn, resolve, reject, retryCount, retryInterval, timeout } = this.queue.shift();
        this.running++;

        this._executeRequest(requestFn, resolve, reject, retryCount, retryInterval, timeout);
    }

    /**
     * 执行请求,并处理重试逻辑
     * @param {Function} requestFn - 请求函数
     * @param {Function} resolve - 成功回调
     * @param {Function} reject - 失败回调
     * @param {number} retryCount - 剩余重试次数
     * @param {number} retryInterval - 重试间隔(毫秒)
     * @param {number} timeout - 请求超时时间(毫秒)
     */
    _executeRequest(requestFn, resolve, reject, retryCount, retryInterval, timeout) {
        let currentRetry = 0;

        const execute = () => {
            const requestPromise = requestFn();
            const timeoutPromise = new Promise((_, reject) => {
                setTimeout(() => {
                    reject(new Error('Request timed out'));
                }, timeout);
            });

            Promise.race([requestPromise, timeoutPromise])
                .then(resolve)
                .catch((error) => {
                    if (currentRetry < retryCount) {
                        if (this.log) {
                            console.warn(`[RequestPool] 任务失败,${currentRetry + 1}次重试中...`);
                        }
                        currentRetry++;
                        setTimeout(execute, retryInterval); // 重试间隔后再次执行
                    } else {
                        if (this.log) {
                            console.error(`[RequestPool] 任务失败,重试次数用完:`, error);
                        }
                        reject(error);
                    }
                })
                .finally(() => {
                    this.running--;
                    this._run(); // 继续处理下一个任务
                });
        };

        execute(); // 开始执行请求
    }

    clear() {
        this.queue = []; // 清空队列
        this.running = 0; // 重置运行中的请求数
        this.activeRequests.clear(); // 清空正在执行的请求
        if (this.log) {
            console.log('[RequestPool] 请求池已清空');
        }
    }

    /**
     * 取消一个正在进行的请求
     * @param {any} identifier - 请求的唯一标识符
     */
    cancel(identifier) {
        const requestId = this._generateRequestId(() => {}, identifier);
        if (this.activeRequests.has(requestId)) {
            const requestPromise = this.activeRequests.get(requestId);
            if (requestPromise.cancel) {
                requestPromise.cancel(); // 调用取消方法
            }
            this.activeRequests.delete(requestId);
            if (this.log) {
                console.log(`[RequestPool] 请求已取消: ${requestId}`);
            }
        } else {
            if (this.log) {
                console.warn(`[RequestPool] 未找到请求: ${requestId}`);
            }
        }
    }
}
// 模拟接口,随机失败
function mockApi(id) {
    const delay = Math.random() * 1500 + 2000; // 随机延迟 2 ~ 3.5 秒
    const shouldFail = Math.random() < 0.5; // 50% 的概率失败
    console.log(`[${id}] 请求开始...`);
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (shouldFail) {
                console.log(`[${id}] 请求失败,将重试`);
                reject(new Error(`任务${id}失败`));
            } else {
                console.log(`[${id}] 请求完成!`);
                resolve(`任务:${id}的结果`);
            }
        }, delay);
    });
}

// 创建请求池,限制并发为 2
const pool = new RequestPool(2, { log: true });

// 添加任务,每个任务失败时重试一次,重试间隔为 1000ms,超时时间为 5000ms
for (let i = 1; i <= 5; i++) {
    pool.add(() => mockApi(i), i, 2, 1000, 5000)
        .then(result => {
            console.log(`[${i}] 收到结果: ${result}`);
        })
        .catch(error => {
            console.error(`[${i}] 最终失败: ${error.message}`);
        });
}

// 添加重复请求
pool.add(() => mockApi(1), 1, 2, 1000, 5000)
    .then(result => {
        console.log(`[重复请求1] 收到结果: ${result}`);
    })
    .catch(error => {
        console.error(`[重复请求1] 最终失败: ${error.message}`);
    });

 

它是如何工作的?(简洁)

/**
 * 一个简单的请求池/请求队列,用于控制并发
 * @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();   // 尝试运行下一个
        });
    }
  }
}


        // 1.模拟二个慢接口
        function mockApi(id) {
            const delay = Math.random() * 1500 + 2000;// 随机延迟 2 ~ 3.5 秒
            console.log(`[${id}]岁 请求开始.··`);
            return new Promise(resolve => {
                setTimeout(() => {
                    console.log(`[${id}] 请求完成!`);
                    resolve(`任务:${id}};的结果`);
                }, delay);
            });
        }
        // 2.创建一个请求池,限制并发为 2
        const pool = new RequestPool(2);

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

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

以后再遇到需要批量发请求的场景,别再用 Promise.all 一股脑全发出去了。
Promise.all的弊端:
如果并发数过多,可能会超过服务器的最大连接数限制或者导致客户端的资源耗尽(如内存溢出)

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

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

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐