异步操作中止机制 AbortController是现代JavaScript中管理异步操作的重要工具,提供统一的中止机制。
它通过signal属性与fetch请求、事件监听器等Web API结合使用,能有效避免内存泄漏和竞态条件。
主要应用场景包括:
- 取消用户操作(搜索/上传)
- 组件生命周期管理
- 超时控制和事件清理
使用时需注意:
- 每次创建新控制器
- 正确处理AbortError
- 及时清理资源
- 并考虑浏览器兼容性
最佳实践建议结合防抖/节流使用,为耗时操作提供中止选项。
AbortController() 详解
1. 基本概念
AbortController 是一个用于中止一个或多个异步操作的控制器对象。它常与以下 API 结合使用:
-
fetch()请求 -
addEventListener()事件监听器 -
其他 Web API(如
ReadableStream、WebSocket等)
2. 基本使用
创建控制器
// 创建一个 AbortController 实例
const controller = new AbortController();
// 获取关联的 AbortSignal
const signal = controller.signal;
3. 与 fetch() 结合使用
3.1 中止单个 fetch 请求
const controller = new AbortController();
const signal = controller.signal;
// 发起 fetch 请求,传入 signal
fetch('https://api.example.com/data', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('请求被中止');
} else {
console.error('请求失败:', error);
}
});
// 在需要时中止请求
setTimeout(() => {
controller.abort(); // 中止请求
}, 5000); // 5秒后中止
3.2 同时中止多个请求
const controller = new AbortController();
const signal = controller.signal;
// 发起多个使用同一个 signal 的请求
const requests = [
fetch('/api/user', { signal }),
fetch('/api/posts', { signal }),
fetch('/api/comments', { signal })
];
// 全部使用相同的 signal
Promise.all(requests)
.then(responses => Promise.all(responses.map(r => r.json())))
.catch(error => {
if (error.name === 'AbortError') {
console.log('所有请求都被中止');
}
});
// 中止所有请求
document.getElementById('cancel-btn').addEventListener('click', () => {
controller.abort();
});
4. 与 addEventListener() 结合使用
4.1 管理事件监听器
const controller = new AbortController();
const signal = controller.signal;
// 使用同一个 signal 注册多个事件
button.addEventListener('click', () => {
console.log('按钮被点击');
}, { signal });
input.addEventListener('input', (e) => {
console.log('输入:', e.target.value);
}, { signal });
// 移除所有通过该 signal 注册的事件监听器
controller.abort();
4.2 组件清理示例
class MyComponent {
constructor() {
this.controller = new AbortController();
this.signal = this.controller.signal;
this.setupEventListeners();
}
setupEventListeners() {
// 所有事件都使用同一个 signal
document.addEventListener('click', this.handleClick.bind(this), {
signal: this.signal
});
window.addEventListener('resize', this.handleResize.bind(this), {
signal: this.signal
});
document.addEventListener('keydown', this.handleKeydown.bind(this), {
signal: this.signal
});
}
handleClick() { /* ... */ }
handleResize() { /* ... */ }
handleKeydown() { /* ... */ }
// 组件销毁时清理所有事件监听器
destroy() {
this.controller.abort();
}
}
// 使用
const component = new MyComponent();
// ... 稍后 ...
component.destroy(); // 清理所有事件监听器
5. AbortSignal 属性和方法
5.1 属性
const controller = new AbortController();
const signal = controller.signal;
console.log(signal.aborted); // false,是否已中止
console.log(signal.reason); // undefined,中止原因
controller.abort('用户取消了操作');
console.log(signal.aborted); // true
console.log(signal.reason); // '用户取消了操作'
5.2 监听中止事件
const controller = new AbortController();
const signal = controller.signal;
// 监听中止事件
signal.addEventListener('abort', () => {
console.log('signal 被中止');
console.log('原因:', signal.reason);
});
// 触发中止
controller.abort('手动中止');
6. 高级用法
6.1 超时自动中止
function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const signal = controller.signal;
// 设置超时
const timeoutId = setTimeout(() => {
controller.abort(new Error(`请求超时 (${timeout}ms)`));
}, timeout);
// 发起请求
return fetch(url, { ...options, signal })
.finally(() => clearTimeout(timeoutId));
}
// 使用
fetchWithTimeout('https://api.example.com/data', {}, 3000)
.then(response => response.json())
.catch(error => {
if (error.name === 'AbortError') {
console.error('请求超时');
}
});
6.2 组合多个 signal
function createCombinedSignal(signals) {
const controller = new AbortController();
signals.forEach(signal => {
if (signal.aborted) {
controller.abort(signal.reason);
} else {
signal.addEventListener('abort', () => {
controller.abort(signal.reason);
});
}
});
return controller.signal;
}
// 使用
const controller1 = new AbortController();
const controller2 = new AbortController();
const combinedSignal = createCombinedSignal([
controller1.signal,
controller2.signal
]);
fetch('/api/data', { signal: combinedSignal });
// 任意一个 controller.abort() 都会中止 fetch
controller1.abort('controller1 中止');
6.3 与异步迭代器结合
async function* asyncGenerator(signal) {
let i = 0;
while (!signal.aborted) {
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 1000));
yield i++;
}
console.log('生成器被中止');
}
const controller = new AbortController();
(async () => {
for await (const value of asyncGenerator(controller.signal)) {
console.log(value);
if (value >= 5) {
controller.abort();
}
}
})();
7. 实际应用场景
7.1 搜索框防抖 + 请求取消
class SearchComponent {
constructor() {
this.controller = null;
this.timeoutId = null;
this.input = document.getElementById('search-input');
this.results = document.getElementById('search-results');
this.init();
}
init() {
this.input.addEventListener('input', (e) => {
this.handleSearch(e.target.value);
});
}
handleSearch(query) {
// 取消之前的请求
if (this.controller) {
this.controller.abort();
}
// 清除之前的防抖定时器
clearTimeout(this.timeoutId);
// 设置新的防抖
this.timeoutId = setTimeout(() => {
this.performSearch(query);
}, 300);
}
performSearch(query) {
// 创建新的控制器
this.controller = new AbortController();
fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: this.controller.signal
})
.then(response => response.json())
.then(data => {
this.displayResults(data);
})
.catch(error => {
if (error.name !== 'AbortError') {
console.error('搜索失败:', error);
}
});
}
displayResults(data) {
// 显示结果...
}
}
7.2 表单提交防重复
class FormSubmitter {
constructor(formId) {
this.form = document.getElementById(formId);
this.submitButton = this.form.querySelector('[type="submit"]');
this.controller = null;
this.init();
}
init() {
this.form.addEventListener('submit', async (e) => {
e.preventDefault();
// 如果已有请求在进行,先中止
if (this.controller) {
this.controller.abort();
}
await this.submitForm();
});
}
async submitForm() {
this.controller = new AbortController();
this.submitButton.disabled = true;
const formData = new FormData(this.form);
try {
const response = await fetch('/api/submit', {
method: 'POST',
body: formData,
signal: this.controller.signal
});
const result = await response.json();
this.handleSuccess(result);
} catch (error) {
if (error.name !== 'AbortError') {
this.handleError(error);
}
} finally {
this.submitButton.disabled = false;
this.controller = null;
}
}
handleSuccess(result) { /* ... */ }
handleError(error) { /* ... */ }
// 允许外部中止
abort() {
if (this.controller) {
this.controller.abort();
}
}
}
8. 注意事项和最佳实践
8.1 错误处理
try {
const response = await fetch(url, { signal });
// 处理响应
} catch (error) {
if (error.name === 'AbortError') {
// 请求被中止,通常是用户操作
console.log('操作被取消');
} else {
// 其他错误
console.error('请求失败:', error);
}
}
8.2 内存管理
// 正确:使用 finally 清理资源
const controller = new AbortController();
try {
// ... 异步操作
} finally {
// 清理工作
controller.abort();
}
// 或使用 try...catch...finally
try {
// ...
} catch (error) {
// ...
} finally {
controller.abort();
}
8.3 浏览器兼容性
// 检查是否支持 AbortController
if (typeof AbortController !== 'undefined') {
// 使用 AbortController
const controller = new AbortController();
} else {
// 降级方案
console.warn('AbortController 不支持,使用传统方式');
}
8.4 不要重复使用
// ❌ 错误:重复使用已中止的 controller
const controller = new AbortController();
controller.abort();
// 这个 signal 已经是中止状态
fetch(url, { signal: controller.signal }); // 会立即被拒绝
// ✅ 正确:每次创建新的
function makeRequest() {
const controller = new AbortController();
return fetch(url, { signal: controller.signal });
}
总结
AbortController 是现代 JavaScript 中管理异步操作生命周期的重要工具:
-
核心优势:
-
统一的中止机制
-
更好的内存管理
-
避免竞态条件
-
提高用户体验
-
-
适用场景:
-
取消用户触发的操作(搜索、上传、下载)
-
组件生命周期管理
-
超时控制
-
清理事件监听器
-
-
最佳实践:
-
及时清理不再需要的控制器
-
正确处理
AbortError -
为长时间运行的操作提供中止机制
-
结合防抖/节流使用
-