JS 红绿灯考题与 AbortController 实战解析

38 阅读3分钟

在前端面试中,红绿灯问题是一个非常经典的考题,它能考察候选人对 异步编程、Promise、async/await、流程控制 的掌握情况。
同时,现代前端开发中还常常涉及 AbortController信号(signal) 的使用,用于在组件卸载或用户操作时 中止异步任务,避免内存泄漏。

本文就结合一个红绿灯的例子,讲解如何实现 红绿灯轮询,以及如何通过 AbortController 让它停下来。


一、为什么要考红绿灯?

  • 异步变同步:JS 单线程,没有内置 sleep,需要自己封装。
  • 流程控制:考察候选人能否把异步逻辑写出像同步一样的“顺序”效果。
  • Promise 与 async/await:不同写法的掌握情况。
  • 中止机制:结合 AbortController,可以延伸考点到 信号传递、资源清理、内存泄漏

二、最基础的 Sleep 封装

JS 没有 sleep,我们可以用 Promise + setTimeout 来封装一个:

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

这样就能配合 await 让异步逻辑变得像同步:

(async () => {
  console.log('red');
  await sleep(1000);
  console.log('green');
  await sleep(2000);
  console.log('yellow');
})();

三、红绿灯循环(async/await 版本)

async function trafficLight() {
  const seq = [    { color: 'red', ms: 1000 },    { color: 'green', ms: 2000 },    { color: 'yellow', ms: 3000 },  ];

  while (true) { // 无限循环
    for (const { color, ms } of seq) {
      console.log(color);
      await sleep(ms); // 等待对应时间
    }
  }
}

trafficLight();

这里 while(true) + await sleep 实现了一个无限循环的红绿灯。


四、thenable 版本(考察 Promise 链)

同样的逻辑,还可以用 Promise then 链来写:

function light(color, ms) {
  console.log(color);
  return new Promise(resolve => setTimeout(resolve, ms));
}

function loop() {
  light('red', 1000)
    .then(() => light('green', 2000))
    .then(() => light('yellow', 3000))
    .then(loop); // 递归
}

loop();

这里考察的是 Promise thenable 链式调用,以及 递归控制流程


五、如何让红绿灯停下来?

上面的代码虽然能无限循环红绿灯,但缺少一个 中止机制
假设这是一个 React 组件,用户切换路由了,但定时器还在跑 → 就会造成 内存泄漏

这时就要用到 AbortController


六、完整示例代码(带开始/暂停按钮)

下面的代码可以在浏览器直接运行,点击按钮来控制红绿灯:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>如何让红绿灯停下来</title>
</head>
<body>
  <button id="start">开始</button>
  <button id="pause">暂停</button>
  <div id="status"></div>
  <script>
    const statusBox = document.getElementById('status');

    // 可中止的 sleep
    const sleep = (ms, signal) => new Promise((resolve, reject) => {
      const timer = setTimeout(resolve, ms);
      signal.addEventListener("abort", () => {
        clearTimeout(timer);
        reject(new DOMException("Aborted", "AbortError"));
      }, { once: true });
    });

    const seq = [
      { color: 'red', ms: 1000 },
      { color: 'yellow', ms: 3000 },
      { color: 'green', ms: 2000 }
    ];

    let controller = null;

    async function trafficLight(signal) {
      while (!signal.aborted) {
        for (const { color, ms } of seq) {
          if (signal.aborted) return;
          console.log(color);
          statusBox.textContent = color;
          try {
            await sleep(ms, signal);
          } catch (err) {
            if (err.name === "AbortError") return;
            throw err;
          }
        }
      }
    }

    document.getElementById('start').addEventListener('click', () => {
      if (controller && !controller.signal.aborted) return;
      controller = new AbortController();
      trafficLight(controller.signal);
    });

    document.getElementById("pause").addEventListener("click", () => {
      if (controller) controller.abort();
    });
  </script>
</body>
</html>

七、核心点解析

  1. sleep 可中止

    • 普通 sleep 只是 setTimeout,无法中断。
    • 这里用 AbortController.signal,一旦触发 abort,就会 clearTimeoutreject
  2. AbortController 的作用

    • controller = new AbortController() 生成一个控制器。
    • controller.signal 就是传递给异步任务的信号。
    • 调用 controller.abort() 会广播“中止”信号。
  3. 避免内存泄漏

    • 如果用户离开页面或组件卸载,调用 abort() 即可立即中断异步逻辑。
    • 否则定时器会一直运行,导致内存泄漏。

八、面试回答思路

如果面试官问到这个题,你可以这样回答:

“红绿灯问题主要考察 Promise、async/await、sleep 封装,以及如何让异步变同步。
我会先写一个 sleep 函数配合 await,实现无限循环的红绿灯逻辑。
在实际项目中,还需要考虑组件卸载导致的内存泄漏问题,所以我会用 AbortController 给异步任务加一个中止信号,这样用户点击暂停或切换路由时,可以安全地停下红绿灯。
这样不仅考察基本功,还能体现我对工程实践和内存管理的理解。”


✅ 总结:

  • sleep + async/await → 基础红绿灯
  • Promise thenable → 链式调用考察
  • AbortController → 高级考点(中止机制、避免内存泄漏)