- 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 请求。
基本模式
- 创建一个
AbortController实例。 - 将其
signal属性作为fetch请求的options之一传入。 - 在需要的时候,调用
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();