JavaScript 中的可取消异步操作与 AbortController API
字数 1050 2025-12-14 05:32:31
JavaScript 中的可取消异步操作与 AbortController API
一、问题描述
在JavaScript的异步编程中,经常会遇到需要取消正在进行的异步操作(如网络请求、定时任务等)的场景。传统的Promise一旦开始就无法从外部取消,这可能导致资源浪费或竞态条件。ES2021引入的AbortController与AbortSignal提供了标准化的取消机制。本知识点将深入讲解其工作原理、使用方法和实际应用。
二、核心概念解析
1. AbortController
AbortController是一个控制器对象,用于创建和发送取消信号。
const controller = new AbortController();
主要属性和方法:
signal:只读属性,返回一个AbortSignal对象abort():方法,调用时触发取消信号
2. AbortSignal
AbortSignal是一个信号对象,用于监听取消事件。
const signal = controller.signal;
关键特性:
aborted:布尔属性,表示是否已取消reason:取消的原因(调用abort(reason)时传入)onabort:事件监听器,或使用addEventListener('abort', handler)
三、使用场景与实现
场景1:取消Fetch请求
// 创建控制器
const controller = new AbortController();
const { signal } = controller;
// 发起请求
fetch('https://api.example.com/data', { signal })
.then(response => response.json())
.then(data => console.log('成功:', data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求已取消');
} else {
console.error('请求失败:', err);
}
});
// 5秒后取消请求
setTimeout(() => {
controller.abort('用户手动取消');
}, 5000);
执行过程分析:
- Fetch API内部会监听signal的变化
- 当调用
controller.abort()时,signal的aborted变为true - Fetch会自动中断请求并抛出
AbortError - 错误处理中通过
err.name识别取消类型
场景2:自定义可取消异步函数
function cancellableDelay(ms, signal) {
return new Promise((resolve, reject) => {
// 初始检查:如果已经取消,立即拒绝
if (signal.aborted) {
reject(new DOMException('操作已取消', 'AbortError'));
return;
}
const timeoutId = setTimeout(() => {
resolve('延迟完成');
}, ms);
// 监听取消信号
const abortHandler = () => {
clearTimeout(timeoutId);
reject(new DOMException('操作已取消', 'AbortError'));
};
// 一次性监听,避免内存泄漏
if (signal.addEventListener) {
signal.addEventListener('abort', abortHandler, { once: true });
} else {
// 兼容旧版API
signal.onabort = abortHandler;
}
});
}
// 使用示例
const controller = new AbortController();
cancellableDelay(3000, controller.signal)
.then(result => console.log(result))
.catch(err => {
if (err.name === 'AbortError') {
console.log('延迟被取消');
}
});
// 1秒后取消
setTimeout(() => controller.abort(), 1000);
四、高级用法
1. 信号组合:多个操作共享取消
// 创建主控制器
const mainController = new AbortController();
// 为子操作创建关联信号
function createLinkedSignal(originalSignal) {
const linkedController = new AbortController();
// 当原信号取消时,也取消关联信号
if (originalSignal.aborted) {
linkedController.abort(originalSignal.reason);
} else {
originalSignal.addEventListener('abort', () => {
linkedController.abort(originalSignal.reason);
}, { once: true });
}
return linkedController.signal;
}
// 使用示例
const linkedSignal = createLinkedSignal(mainController.signal);
// 两个请求共享取消
fetch('/api1', { signal: mainController.signal });
fetch('/api2', { signal: linkedSignal });
// 取消所有
mainController.abort('用户取消所有请求');
2. 超时自动取消
function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const { signal } = controller;
// 创建超时控制器
const timeoutController = new AbortController();
const timeoutId = setTimeout(() => {
timeoutController.abort(new DOMException('请求超时', 'TimeoutError'));
}, timeout);
// 合并信号:任意一个取消就整体取消
const mergedSignal = (() => {
const mergedController = new AbortController();
function abortAll(reason) {
mergedController.abort(reason);
}
signal.addEventListener('abort', () => abortAll(signal.reason));
timeoutController.signal.addEventListener('abort',
() => abortAll(timeoutController.signal.reason));
return mergedController.signal;
})();
// 清理定时器
mergedSignal.addEventListener('abort', () => {
clearTimeout(timeoutId);
}, { once: true });
// 发起请求
return fetch(url, { ...options, signal: mergedSignal });
}
五、内存管理与最佳实践
1. 避免内存泄漏
function safeAsyncOperation(signal) {
return new Promise((resolve, reject) => {
const abortHandler = () => {
cleanupResources(); // 清理资源
reject(new DOMException('取消', 'AbortError'));
};
// 使用 once 选项确保只触发一次
signal.addEventListener('abort', abortHandler, { once: true });
// 操作完成后移除监听器
doAsyncWork()
.then(result => {
signal.removeEventListener('abort', abortHandler);
resolve(result);
})
.catch(err => {
signal.removeEventListener('abort', abortHandler);
reject(err);
});
});
}
2. 竞态条件处理
class RequestManager {
constructor() {
this.currentController = null;
}
async fetchData(url) {
// 取消之前的请求
if (this.currentController) {
this.currentController.abort('新请求覆盖');
}
// 创建新控制器
this.currentController = new AbortController();
try {
const response = await fetch(url, {
signal: this.currentController.signal
});
return await response.json();
} catch (err) {
if (err.name !== 'AbortError') {
throw err;
}
// 静默处理取消错误
} finally {
this.currentController = null;
}
}
}
六、兼容性与polyfill
// 简单的polyfill(功能简化版)
if (typeof AbortController === 'undefined') {
class AbortControllerPolyfill {
constructor() {
this._aborted = false;
this._listeners = [];
}
get signal() {
return {
aborted: this._aborted,
addEventListener: (event, handler) => {
if (event === 'abort') {
this._listeners.push(handler);
}
},
removeEventListener: (event, handler) => {
if (event === 'abort') {
const index = this._listeners.indexOf(handler);
if (index > -1) this._listeners.splice(index, 1);
}
}
};
}
abort(reason) {
if (!this._aborted) {
this._aborted = true;
this._listeners.forEach(handler => handler());
}
}
}
window.AbortController = AbortControllerPolyfill;
}
七、实际应用场景
- 搜索自动完成:用户连续输入时取消之前的请求
- 文件上传:允许用户取消上传过程
- 轮询请求:离开页面时停止轮询
- 多个并行请求:一个失败时取消其他相关请求
- 动画序列:中断正在进行的动画
八、注意事项
- 错误处理:始终检查
err.name === 'AbortError'来区分取消和其他错误 - 资源清理:取消后要清理定时器、事件监听器等资源
- 信号复用:不要重复使用已取消的signal
- 传播机制:子操作应该响应父信号的取消
通过AbortController,JavaScript终于有了官方的异步取消机制,使得异步操作的管理更加精细和可控。