Published on

AbortController 与 AbortSignal

在复杂的 Web 应用中,异步操作(如数据请求)的生命周期管理是一个关键问题。用户在请求完成前可能会离开页面、关闭模态框或发起新的请求,导致先前的操作变得多余。若不加以处理,这些“悬空”的操作不仅会浪费网络和计算资源,还可能引发竞态条件 (race conditions) 和组件状态异常。AbortController 及其关联的 AbortSignal 接口,是 W3C 标准化的、用于优雅地、可预见地终止 (abort) 一个或多个 Web 操作的权威解决方案。

核心机制:控制器与信号

AbortController 的设计遵循一个清晰的“控制器-信号”模式。

  • AbortController: 这是一个一次性的控制器对象,其唯一的作用就是生成一个信号,并在需要时发出“中止”指令。
    • new AbortController(): 创建一个新的控制器实例。
    • controller.signal: 一个只读属性,返回一个 AbortSignal 对象实例。
    • controller.abort(reason?): 核心方法。调用此方法会向其关联的 AbortSignal 发出一个中止信号。
  • AbortSignal: 这是一个信号对象,它充当了控制器与需要被中止的异步操作之间的“联络员”。
    • 传递: 开发者将 signal 对象传递给异步 API(如 fetch)。
    • 监听: 异步 API 在内部监听此 signal 的状态。
    • 状态: signal.aborted 是一个只读布尔值,一旦 controller.abort() 被调用,它会变为 true
    • 事件: signal 也是一个 EventTarget,当 controller.abort() 被调用时,它会触发一个 abort 事件。

fetch 请求的取消

AbortController 最常见的应用场景是取消 fetch 请求。

基本模式

  1. 创建一个 AbortController 实例。
  2. 将其 signal 属性作为 fetch 请求的 options 之一传入。
  3. 在需要的时候,调用 controller.abort()
中止 fetch 请求

// 1. 创建控制器
const controller = new AbortController();
const signal = controller.signal;

const downloadBtn = document.getElementById('download');
const abortBtn = document.getElementById('abort');

downloadBtn.addEventListener('click', async () => {
  try {
    console.log('开始下载...');
    // 2. 将 signal 传入 fetch
    const response = await fetch('/api/large-file', { signal });
    const data = await response.json();
    console.log('下载完成:', data);
  } catch (err) {
    // 4. 当 fetch 被中止时,其 Promise 会 reject
    if (err.name === 'AbortError') {
      console.log('下载已被用户中止。');
    } else {
      console.error('下载出错:', err);
    }
  }
});

// 3. 在任意位置调用 abort()
abortBtn.addEventListener('click', () => {
  controller.abort();
});

中止多个请求

一个 AbortController 实例可以用于同时控制多个异步操作。

一对多控制

只需将同一个 signal 对象传递给多个 fetch 调用。随后,只需调用一次 controller.abort(),所有与该 signal 关联的 fetch 请求都会被同时中止。这在需要一次性取消页面上所有进行中请求的场景下非常有用。

典型应用场景

在 React useEffect 中清理副作用

这是 AbortController 在现代组件化框架中的核心应用,用于防止在组件卸载后,异步操作的回调尝试更新已卸载的组件状态。

import React, { useState, useEffect } from 'react';

function UserProfile({ id }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // 1. 在 effect 内部创建 AbortController
    const controller = new AbortController();
    
    const fetchUser = async () => {
      try {
        const response = await fetch(`/api/users/${id}`, { 
          signal: controller.signal // 2. 传递 signal
        });
        const data = await response.json();
        setUser(data);
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error(error);
        }
      }
    };

    fetchUser();

    // 3. 返回的清理函数会在 effect 重新运行时或组件卸载时被调用
    return () => {
      controller.abort(); // 4. 中止请求
    };
  }, [id]); // 当 id 变化时,会先中止上一次的请求,再发起新的请求

  // ...
}
避免竞态条件

上述 useEffect 的模式,当 id prop 快速变化时,清理函数会中止上一个仍在进行中的请求。这有效避免了竞态条件,即旧的、较慢的请求结果不会意外地覆盖新的、正确的请求结果。

AbortSignal 的更广泛应用

AbortSignal 是一个通用的信号标准,越来越多的 Web API 开始支持它。例如,它可以用于移除事件监听器。

const controller = new AbortController();
const button = document.getElementById('myBtn');

button.addEventListener('click', () => {
  console.log('Clicked!');
}, { signal: controller.signal }); // 传入 signal

// 稍后...
// 调用 abort() 会自动移除上面的 click 监听器,无需 removeEventListener
controller.abort();