在前端面试中,红绿灯问题是一个非常经典的考题,它能考察候选人对 异步编程、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>
七、核心点解析
-
sleep 可中止
- 普通
sleep只是setTimeout,无法中断。 - 这里用
AbortController.signal,一旦触发abort,就会clearTimeout并reject。
- 普通
-
AbortController 的作用
controller = new AbortController()生成一个控制器。controller.signal就是传递给异步任务的信号。- 调用
controller.abort()会广播“中止”信号。
-
避免内存泄漏
- 如果用户离开页面或组件卸载,调用
abort()即可立即中断异步逻辑。 - 否则定时器会一直运行,导致内存泄漏。
- 如果用户离开页面或组件卸载,调用
八、面试回答思路
如果面试官问到这个题,你可以这样回答:
“红绿灯问题主要考察 Promise、async/await、sleep 封装,以及如何让异步变同步。
我会先写一个sleep函数配合await,实现无限循环的红绿灯逻辑。
在实际项目中,还需要考虑组件卸载导致的内存泄漏问题,所以我会用AbortController给异步任务加一个中止信号,这样用户点击暂停或切换路由时,可以安全地停下红绿灯。
这样不仅考察基本功,还能体现我对工程实践和内存管理的理解。”
✅ 总结:
- sleep + async/await → 基础红绿灯
- Promise thenable → 链式调用考察
- AbortController → 高级考点(中止机制、避免内存泄漏)