在前端开发中,我们经常需要处理复杂的异步流程。一个经典的面试题就是“红绿灯”问题:如何让红灯、黄灯、绿灯按照固定顺序循环亮起。但这只是开始,真正的挑战在于——如何优雅地让这个循环停下来?本文将通过一个完整的 HTML 示例,深入解析如何实现一个可开始、可暂停的红绿灯系统,揭示 AbortController、Promise 和事件循环在实际应用中的精妙配合。
一、问题背景:从“无限循环”到“可控流程”
一个简单的红绿灯可以通过 async/await 和 setTimeout 轻松实现:
async function trafficLight() {
while(true) {
console.log('🔴');
await sleep(1000);
console.log('🟡');
await sleep(3000);
console.log('🟢');
await sleep(2000);
}
}
但这种实现存在严重问题:
- 无法停止:一旦开始,程序将永远运行。
- 资源浪费:即使用户离开页面,循环仍在进行。
- 用户体验差:缺乏交互控制。
核心需求:我们需要一个能随时暂停的红绿灯。
二、技术基石:AbortController 与可中断的 sleep
1. AbortController:异步任务的“遥控器”
AbortController 是 Web API 提供的用于中止异步操作的接口。它包含:
signal:一个AbortSignal对象,作为“信号旗”传递给异步任务。abort():调用此方法,会触发signal上的abort事件。
2. 实现可中断的 sleep 函数
关键在于让 sleep 函数能响应 AbortSignal:
const sleep = (ms, signal) => new Promise((resolve, reject) => {
// 设置定时器
const timer = setTimeout(resolve, ms);
// 监听 abort 事件
signal.addEventListener("abort", () => {
clearTimeout(timer); // 清除定时器,防止内存泄漏
reject(new DOMException("Aborted", "AbortError")); // 拒绝 Promise
}, { once: true }); // 事件触发一次后自动移除
});
✅ 精妙之处:
sleep不再是一个简单的延迟,而是一个可被外部信号中断的异步任务。
三、核心逻辑:红绿灯的“启动-暂停”机制
1. 状态管理
使用一个全局的 controller 变量来管理当前的 AbortController 实例:
let controller = null;
- 开始:创建新的
AbortController。 - 暂停:调用
controller.abort()。
2. 主循环:trafficLight 函数
async function trafficLight(signal) {
while(!signal.aborted) { // 检查信号是否已被中止
for (const {color, ms} of seq) {
if (signal.aborted) return; // 再次检查,确保及时退出
// 更新 UI
console.log(color);
statusBox.textContent = color;
// 等待,但可被中断
try {
await sleep(ms, signal);
} catch(err) {
if (err.name === "AbortError") return; // 安静退出
throw err; // 重新抛出其他错误
}
}
}
}
关键设计:
- 双重检查:
while循环和for循环内部都检查signal.aborted,确保响应迅速。 - 错误处理:
sleep被中止时会reject,需用try-catch捕获AbortError并优雅退出。
3. 事件绑定:用户交互
// 开始按钮
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();
});
- 防重复启动:检查当前
controller是否有效,避免多个实例并发运行。 - 一键暂停:调用
abort()即可中止所有相关操作。
四、代码精要解析
| 代码片段 | 作用 | 设计智慧 |
|---|---|---|
signal.addEventListener(..., { once: true }) | 事件监听器只执行一次 | 避免内存泄漏,符合“中止即终结”的语义 |
clearTimeout(timer) | 清除未完成的定时器 | 资源清理,防止不必要的回调执行 |
reject(new DOMException(...)) | 使用标准 DOM 异常 | 与 fetch 等原生 API 的中止行为保持一致 |
while(!signal.aborted) | 循环条件检查 | 主动退出,避免不必要的迭代 |
五、实际应用场景
这种模式不仅适用于红绿灯,还可广泛应用于:
- 长轮询(Long Polling):用户切换页面时,中止后台数据轮询。
- 文件上传/下载:用户点击“取消”,立即中止传输。
- 动画序列:用户交互时,暂停复杂的 CSS/JS 动画。
- 游戏循环:暂停游戏,保存状态。
六、总结:控制异步,就是控制用户体验
这个红绿灯示例远不止一个面试题,它揭示了现代前端开发的核心思想:
- 异步任务必须是可控的:不能任其“野蛮生长”。
AbortController是关键工具:它提供了一种标准化的“取消”机制。Promise是基础:通过resolve/reject,我们可以将任何延迟操作转化为可中断的异步任务。- 用户体验至上:让用户能随时“暂停”,是尊重用户控制权的体现。
最终结论:让红绿灯停下来,本质上是将不可控的无限循环,转变为可被外部信号中断的有限状态机。掌握这一模式,你就能在复杂的前端应用中,游刃有余地指挥每一个异步任务,让它们“该亮时亮,该停时停”,真正实现流畅、可靠的用户体验。