前端一面:并发控制、重试与轮询题目精讲

在“异步/网络”这一块,一面常见的进阶题集中在:并发数控制、请求重试策略、轮询等。这一篇挑几道典型题,重点讲清楚思路而不是堆代码。

这类题的典型描述是:

有一组异步任务(通常是返回 Promise 的函数),要求同一时间最多并发执行 N 个,全部完成后返回结果。

高层思路:

  • 维护一个队列存待执行任务;
  • 维护一个计数器 running 表示当前运行中的数量;
  • running < limit 时,从队列取任务执行,并在任务完成后递减计数,再触发下一任务。

比起记代码,更重要的是你能否先把这三点讲清楚。

重试题常见的变体:

  • 固定重试次数(例如最多 3 次);
  • 带延迟/指数退避(每次重试等待时间增加);
  • 区分“可重试错误”和“不可重试错误”(如 5xx vs 4xx)。

常见思路:

  • 把“执行一次请求 + 必要等待”的逻辑封装成一个递归或循环;
  • 用一个计数器记录已经尝试的次数,超过上限就直接 reject;
  • 可以根据错误类型决定是否继续重试。

轮询题通常描述为:

每隔一段时间请求一次接口,直到返回某个状态或达到最大尝试次数。

高层思路:

  • 使用 setTimeoutsetInterval 控制节奏;
  • 在回调中判断是否满足条件:
    • 满足 → resolve,停止轮询;
    • 不满足 → 如果未达最大次数,则安排下一次调用;否则 reject/给出超时提示。

这类题重点是控制逻辑清晰,而不是用某个“巧妙 API”一行写完。

题目示例:实现一个 Scheduler,构造时传入最大并发数,add 方法接收一个返回 Promise 的函数,要求同一时间最多执行 limit 个任务。

参考实现思路(可简化):

 1class Scheduler {
 2  constructor(limit) {
 3    this.limit = limit;
 4    this.running = 0;
 5    this.queue = [];
 6  }
 7
 8  add(task) {
 9    return new Promise((resolve, reject) => {
10      const run = () => {
11        this.running++;
12        task()
13          .then(resolve, reject)
14          .finally(() => {
15            this.running--;
16            if (this.queue.length > 0) {
17              const next = this.queue.shift();
18              next();
19            }
20          });
21      };
22
23      if (this.running < this.limit) {
24        run();
25      } else {
26        this.queue.push(run);
27      }
28    });
29  }
30}

答题时可以强调:

  • 用队列 + 计数器来做“流量控制”;
  • finally 中递减计数并触发队列里的下一个任务;
  • add 返回的也是 Promise,方便调用方用 await 聚合结果。

题目示例:封装一个 requestWithRetry(fn, times, delay),失败时最多重试 times 次,每次间隔 delay 毫秒。

参考实现(简化版):

 1function sleep(ms) {
 2  return new Promise((resolve) => setTimeout(resolve, ms));
 3}
 4
 5async function requestWithRetry(fn, times = 3, delay = 1000) {
 6  let lastError;
 7  for (let i = 0; i < times; i++) {
 8    try {
 9      return await fn();
10    } catch (e) {
11      lastError = e;
12      if (i < times - 1) {
13        await sleep(delay);
14      }
15    }
16  }
17  throw lastError;
18}

可以顺带提到的点:

  • 实战中可以根据错误类型决定是否重试(例如只对网络错误/5xx 重试);
  • 也可以采用指数退避策略(delay 逐次增加)来避免打爆服务。

题目示例:实现一个 poll(fn, interval, maxTimes),定期调用 fn,直到返回某个条件或达到最大次数。

参考实现思路:

 1function poll(fn, interval = 1000, maxTimes = 10) {
 2  let count = 0;
 3
 4  return new Promise((resolve, reject) => {
 5    const execute = async () => {
 6      try {
 7        const result = await fn();
 8        if (result.done) {
 9          resolve(result);
10        } else if (count >= maxTimes) {
11          reject(new Error("poll timeout"));
12        } else {
13          count++;
14          setTimeout(execute, interval);
15        }
16      } catch (e) {
17        reject(e);
18      }
19    };
20
21    execute();
22  });
23}

答题时可以补充:

  • 根据业务需求,可以把 result.done 换成任意条件;
  • 如果需要长时间轮询,应该注意取消/清理机制(例如返回一个可以停止轮询的函数)。

这几道题不要求“一字不差记住代码”,面试官更期待的是你能先用自然语言描述思路,再写出结构清晰、易于沟通的实现。