手写题之:“红绿灯”深度解析Fetch 中止与异步控制的艺术(下)

116 阅读4分钟

在前端开发中,我们经常需要处理复杂的异步流程。一个经典的面试题就是“红绿灯”问题:如何让红灯、黄灯、绿灯按照固定顺序循环亮起。但这只是开始,真正的挑战在于——如何优雅地让这个循环停下来?本文将通过一个完整的 HTML 示例,深入解析如何实现一个可开始、可暂停的红绿灯系统,揭示 AbortControllerPromise 和事件循环在实际应用中的精妙配合。


一、问题背景:从“无限循环”到“可控流程”

一个简单的红绿灯可以通过 async/awaitsetTimeout 轻松实现:

async function trafficLight() {
  while(true) {
    console.log('🔴');
    await sleep(1000);
    console.log('🟡');
    await sleep(3000);
    console.log('🟢');
    await sleep(2000);
  }
}

但这种实现存在严重问题:

  1. 无法停止:一旦开始,程序将永远运行。
  2. 资源浪费:即使用户离开页面,循环仍在进行。
  3. 用户体验差:缺乏交互控制。

核心需求:我们需要一个能随时暂停的红绿灯。


二、技术基石: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)循环条件检查主动退出,避免不必要的迭代

五、实际应用场景

这种模式不仅适用于红绿灯,还可广泛应用于:

  1. 长轮询(Long Polling):用户切换页面时,中止后台数据轮询。
  2. 文件上传/下载:用户点击“取消”,立即中止传输。
  3. 动画序列:用户交互时,暂停复杂的 CSS/JS 动画。
  4. 游戏循环:暂停游戏,保存状态。

六、总结:控制异步,就是控制用户体验

这个红绿灯示例远不止一个面试题,它揭示了现代前端开发的核心思想:

  • 异步任务必须是可控的:不能任其“野蛮生长”。
  • AbortController 是关键工具:它提供了一种标准化的“取消”机制。
  • Promise 是基础:通过 resolve/reject,我们可以将任何延迟操作转化为可中断的异步任务。
  • 用户体验至上:让用户能随时“暂停”,是尊重用户控制权的体现。

最终结论:让红绿灯停下来,本质上是将不可控的无限循环,转变为可被外部信号中断的有限状态机。掌握这一模式,你就能在复杂的前端应用中,游刃有余地指挥每一个异步任务,让它们“该亮时亮,该停时停”,真正实现流畅、可靠的用户体验。