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

106 阅读4分钟

在现代前端开发中,我们经常需要精确控制异步任务的执行顺序、生命周期和中断机制。本文将围绕“红绿灯”问题(异步变同步)、fetch 请求的中止,以及 AbortControllerPromise 的巧妙应用,进行系统性深度解析。


一、Fetch 能不能中止?—— AbortController 与信号机制

1. 问题背景:内存泄漏风险

在 SPA(单页应用)中,一个常见场景是:

  • 用户在组件 A 发起一个 fetch 请求。
  • 请求耗时较长,尚未返回。
  • 用户切换路由,组件 A 被卸载。
  • 此时,fetch 请求仍在后台进行。
  • 当请求最终返回时,组件已不存在,若尝试更新状态(如 setState),就会导致内存泄漏或报错。

2. 解决方案:AbortController

fetch API 原生支持通过 signal 选项进行中止。AbortController 就是为此设计的。

// 1. 创建 AbortController
const controller = new AbortController();
const { signal } = controller; // 获取 signal 对象

// 2. 发起 fetch 请求,并传入 signal
fetch('/api/data', {
  method: 'GET',
  signal: signal // 关键:将 signal 绑定到请求
})
.then(response => response.json())
.then(data => {
  // 组件可能已卸载,需检查
  if (componentMounted) {
    setData(data);
  }
})
.catch(error => {
  if (error.name === 'AbortError') {
    console.log('请求已被中止');
  } else {
    console.error('请求失败:', error);
  }
});

// 3. 在组件卸载时中止请求(React 示例)
useEffect(() => {
  return () => {
    controller.abort(); // 中止请求,触发 AbortError
  };
}, []);

3. AbortController 的核心机制

  • signal:一个 AbortSignal 对象,作为“信号旗”。传递给 fetchEventSource 等异步操作。
  • abort():调用此方法,会触发 signal 上的 abort 事件,所有监听该信号的操作都会收到中止通知。
  • 跨任务通用:不仅限于 fetch,还可用于 setTimeoutWebSocket、自定义异步任务。
// 用于中止 setTimeout
const controller = new AbortController();
const timeoutId = setTimeout(() => {
  console.log('Timeout!');
}, 5000);

// 在某个条件下中止
controller.signal.addEventListener('abort', () => {
  clearTimeout(timeoutId);
  console.log('Timeout 已中止');
});

// 触发中止
controller.abort();

核心思想信号(Signal)是一种“发布-订阅”模式,用于在异步任务间传递中断指令。


二、红绿灯问题:异步变同步的“顺序控制”

1. 问题描述

模拟红绿灯:

  • 红灯亮 3 秒
  • 黄灯亮 1 秒
  • 绿灯亮 2 秒
  • 循环往复

要求:按顺序执行,不能同时亮

2. 关键技术:sleep 函数的实现

JavaScript 没有内置的 sleep 函数,但我们可以用 PromisesetTimeout 轻松实现:

// 箭头函数一行搞定
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

// 使用
async function trafficLight() {
  while (true) {
    console.log('🔴 红灯亮');
    await sleep(3000); // 等待 3 秒

    console.log('🟡 黄灯亮');
    await sleep(1000); // 等待 1 秒

    console.log('🟢 绿灯亮');
    await sleep(2000); // 等待 2 秒
  }
}

trafficLight();

3. 深入解析:await 如何实现“暂停”

  • sleep(3000) 返回一个 Promise,该 Promise 在 3 秒后被 resolve
  • await 关键字会暂停 async 函数的执行,直到 Promise 状态变为 fulfilled
  • 这期间,JavaScript 引擎可以处理其他任务(如事件、渲染),不会阻塞主线程
  • 3 秒后,Promise resolveawait 表达式完成,函数继续执行下一行。

本质await 将“等待”从阻塞式(Blocking)变为非阻塞式(Non-blocking)的异步等待。

4. 更复杂的红绿灯:支持中断

结合 AbortController,实现可中断的红绿灯:

async function trafficLightWithAbort(signal) {
  const colors = [
    { color: '🔴', duration: 3000 },
    { color: '🟡', duration: 1000 },
    { color: '🟢', duration: 2000 }
  ];

  let index = 0;
  while (!signal.aborted) { // 检查是否被中止
    const { color, duration } = colors[index % colors.length];
    console.log(`${color} 亮`);
    
    // 使用 Promise.race 实现可中断的 sleep
    try {
      await Promise.race([
        sleep(duration),
        new Promise((_, reject) => {
          signal.addEventListener('abort', () => {
            reject(new Error('Traffic light aborted'));
          });
        })
      ]);
    } catch (error) {
      if (error.message === 'Traffic light aborted') {
        console.log('红绿灯已中止');
        return;
      }
    }

    index++;
  }
}

// 使用
const controller = new AbortController();
trafficLightWithAbort(controller.signal);

// 在某个时刻中止
setTimeout(() => {
  controller.abort();
}, 10000);

三、Promise 考题精析

1. 经典题目:Promise.resolve 与微任务队列

console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);

// 输出:1, 4, 3, 2

解析

  1. 14 同步执行。
  2. setTimeout 的回调进入宏任务队列
  3. Promise.then 的回调进入微任务队列
  4. 当前宏任务(脚本)执行完后,先清空微任务队列(输出 3),再执行下一个宏任务(输出 2)。

2. 高级题目:async/await 执行顺序

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end'); // 这行在 await 后,会进入微任务
}

async function async2() {
  console.log('async2');
}

console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
async1();
new Promise(resolve => {
  console.log('promise1');
  resolve();
}).then(() => console.log('promise2'));
console.log('script end');

// 输出:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout

关键点await 后的代码会被包装成微任务。


四、总结:异步控制的三大支柱

技术作用核心场景
Promise封装异步操作,解决回调地狱基础异步处理
async/await以同步语法书写异步代码,await 实现“暂停”顺序控制、简化逻辑
AbortController / signal发送中断信号,主动取消异步任务防止内存泄漏、用户取消操作

最佳实践

  1. sleep = ms => new Promise(r => setTimeout(r, ms)) 实现等待
  2. await 控制异步执行顺序,实现“红绿灯”等场景。
  3. 在组件生命周期中(如 useEffect cleanup),使用 AbortController.abort() 中止未完成的 fetch 请求,防止内存泄漏。
  4. 理解事件循环:宏任务(setTimeout) vs 微任务(Promise.then),这是掌握异步执行顺序的基础。

掌握这些技术,你就能像交通警察一样,精准地指挥前端应用中错综复杂的异步任务流。