AbortSignal 和 AbortController 是 Web 标准中用于取消异步操作的 API。它们提供了一种统一的方式来中止 fetch 请求、事件监听器、以及其他异步操作。
- AbortController 是一个控制器对象,它允许你在需要时中止一个或多个异步操作。
- AbortSignal 是一个信号对象,表示某个操作是否应该被中止。它由 AbortController 创建并暴露给异步操作使用。
为什么需要 Abort
在 Abort API 出现之前,取消异步操作是一个困难的问题:
- fetch 请求无法取消:一旦发起 fetch 请求,就无法在中途取消,即使用户已经导航到其他页面
- 各种异步 API 取消方式不统一:XMLHttpRequest、setTimeout、addEventListener 等都有不同的取消机制
- 资源浪费:无法取消的请求会浪费网络带宽和服务器资源
- 内存泄漏风险:未清理的异步操作可能导致内存泄漏
Abort API 提供了一个统一、标准的解决方案。
javascript
// 创建一个 AbortController 实例
const controller = new AbortController();
// 获取 signal 对象
const signal = controller.signal;
// 使用 signal 发起 fetch 请求
fetch('/api/data', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求被取消');
}
});
// 在某个时刻取消请求
controller.abort();
API 详解
ts
interface AbortController {
readonly signal: AbortSignal; // 返回一个 AbortSignal 对象实例,用于与异步操作通信
abort(reason?: any): void; // 中止操作,可选地提供一个中止原因
}
interface AbortSignal extends EventTarget {
readonly aborted: boolean; // 指示关联的异步操作是否已被中止
onabort: ((this: AbortSignal, ev: Event) => any) | null; // 当关联的 AbortController 调用 abort() 时触发
readonly reason: any; // 中止的原因,默认为 DOMException(name: 'AbortError')
throwIfAborted(): void; // 如果信号已中止,则抛出中止原因
}
var AbortSignal: {
prototype: AbortSignal;
new(): AbortSignal; // 在浏览器中是受保护的,不能直接调用。会抛出异常 「Illegal constructor」
abort(reason?: any): AbortSignal; // 返回一个已经中止的 AbortSignal
any(signals: AbortSignal[]): AbortSignal; // 返回一个新的 AbortSignal,当数组中任何一个 signal 中止时,它也会中止
timeout(milliseconds: number): AbortSignal; // 返回一个将在指定时间后自动中止的 AbortSignal
};
源码实现
以下是 AbortController 和 AbortSignal 的简化实现,帮助理解其工作原理:
typescript
class AbortSignal extends EventTarget {
private _aborted: boolean = false;
private _reason: any = undefined;
get aborted(): boolean {
return this._aborted;
}
get reason(): any {
return this._reason;
}
throwIfAborted(): void {
if (this._aborted) {
throw this._reason;
}
}
// 内部方法:触发中止
_signalAbort(reason: any): void {
if (this._aborted) return;
this._aborted = true;
this._reason = reason ?? new DOMException('signal is aborted', 'AbortError');
// 触发 abort 事件
this.dispatchEvent(new Event('abort'));
}
// 静态方法:创建已中止的 signal
static abort(reason?: any): AbortSignal {
const signal = new AbortSignal();
signal._signalAbort(reason);
return signal;
}
// 静态方法:创建超时的 signal
static timeout(milliseconds: number): AbortSignal {
const signal = new AbortSignal();
setTimeout(() => {
signal._signalAbort(new DOMException('signal timed out', 'TimeoutError'));
}, milliseconds);
return signal;
}
// 静态方法:组合多个 signal
static any(signals: AbortSignal[]): AbortSignal {
const resultSignal = new AbortSignal();
// 检查是否已有 signal 被中止
for (const signal of signals) {
if (signal.aborted) {
resultSignal._signalAbort(signal.reason);
return resultSignal;
}
}
// 监听所有 signal 的 abort 事件
const onAbort = function(this: AbortSignal) {
resultSignal._signalAbort(this.reason);
// 清理其他监听器
for (const signal of signals) {
signal.removeEventListener('abort', onAbort);
}
};
for (const signal of signals) {
signal.addEventListener('abort', onAbort);
}
return resultSignal;
}
}
class AbortController {
private _signal: AbortSignal;
constructor() {
this._signal = new AbortSignal();
}
get signal(): AbortSignal {
return this._signal;
}
abort(reason?: any): void {
this._signal._signalAbort(reason);
}
}
关键设计要点
- 单向通信:AbortController 通过 signal 单向通知异步操作,异步操作不能修改 signal 状态
- 事件驱动:使用 EventTarget 接口,通过事件机制通知监听者
- 不可逆性:一旦 signal 被中止,就无法恢复
- 原因传递:支持传递中止原因,便于错误处理和调试
- 受保护的构造函数:AbortSignal 的构造函数在浏览器中是受保护的,不能直接调用,只能通过特定的工厂方法创建,这确保了 signal 的创建和管理符合规范
实际应用场景
1. 取消 Fetch 请求
最常见的用途是取消网络请求:
javascript
const controller = new AbortController();
// 开始请求
fetch('/api/large-data', { signal: controller.signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求被用户取消');
} else {
console.error('请求失败:', err);
}
});
// 用户点击取消按钮
cancelButton.addEventListener('click', () => {
controller.abort('用户点击取消按钮');
});
解决的问题:
- 避免用户离开页面后仍在后台下载数据
- 减少不必要的网络流量
- 改善用户体验,让用户可以取消长时间运行的请求
2. 请求超时控制
使用 AbortSignal.timeout()
实现请求超时:
javascript
// 5秒后自动超时
fetch('/api/data', {
signal: AbortSignal.timeout(5000)
})
.then(response => response.json())
.catch(err => {
if (err.name === 'TimeoutError') {
console.log('请求超时');
}
});
解决的问题:
- 防止请求无限期挂起
- 为慢速或无响应的服务器设置时间限制
- 提高应用响应性
3. 清理事件监听器
addEventListener 支持 signal 参数,当 signal 中止时自动移除监听器:
javascript
const controller = new AbortController();
// 监听滚动事件
window.addEventListener('scroll', handleScroll, {
signal: controller.signal
});
// 组件卸载时清理
onUnmount(() => {
controller.abort();
});
解决的问题:
- 防止内存泄漏
- 简化事件监听器的清理逻辑
- 避免手动调用 removeEventListener
4. 组合多个中止条件
使用 AbortSignal.any()
组合多个中止条件:
javascript
const userController = new AbortController();
const timeoutSignal = AbortSignal.timeout(5000);
// 用户取消或超时,都会中止请求
fetch('/api/data', {
signal: AbortSignal.any([
userController.signal,
timeoutSignal
])
}).catch(err => {
console.log('请求被中止:', err.message);
});
// 用户点击取消
cancelButton.addEventListener('click', () => {
userController.abort();
});
解决的问题:
- 灵活组合多个中止条件
- 支持复杂的控制流程
- 避免创建多个 controller 的复杂逻辑
5. React/Vue 组件中的异步操作
在组件中使用 AbortSignal 防止内存泄漏:
javascript
// React 示例
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(res => res.json())
.then(data => setUser(data))
.catch(err => {
if (err.name !== 'AbortError') {
console.error('加载用户失败:', err);
}
});
// 清理函数
return () => controller.abort();
}, [userId]);
return <div>{user?.name}</div>;
}
解决的问题:
- 组件卸载后中止正在进行的请求
- 防止在已卸载的组件上设置状态
- 避免竞态条件(race condition)
6. 可中止的自定义异步操作
在自定义异步函数中支持 AbortSignal:
javascript
async function processLargeFile(file, signal) {
const chunks = splitFile(file);
for (const chunk of chunks) {
// 检查是否已中止
signal?.throwIfAborted();
await processChunk(chunk);
}
return 'completed';
}
const controller = new AbortController();
processLargeFile(bigFile, controller.signal)
.then(result => console.log(result))
.catch(err => {
if (err.name === 'AbortError') {
console.log('处理被取消');
}
});
// 用户可以随时取消
cancelButton.onclick = () => controller.abort();
解决的问题:
- 让长时间运行的操作可以被中断
- 统一的取消接口
- 提高代码的可维护性
7. Promise 包装
为不支持 AbortSignal 的 API 添加取消功能:
javascript
function sleep(ms, signal) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => {
clearTimeout(timeoutId);
reject(signal.reason);
});
});
}
const controller = new AbortController();
sleep(5000, controller.signal)
.then(() => console.log('完成'))
.catch(err => console.log('被中止'));
// 2秒后取消
setTimeout(() => controller.abort(), 2000);
解决的问题:
- 为旧 API 添加现代取消机制
- 统一异步操作的取消接口
- 提高代码一致性
总结
AbortSignal 和 AbortController 提供了一种标准、统一的方式来取消异步操作。它们的主要优势包括:
- 标准化:Web 平台统一的取消机制
- 灵活性:支持多种使用场景和组合方式
- 易用性:API 简单直观,易于理解和使用
- 可组合:可以组合多个 signal,支持复杂的控制流程
- 防止内存泄漏:帮助清理不再需要的异步操作
在现代 Web 应用开发中,合理使用 Abort API 可以显著提高应用的性能、响应性和用户体验。