学习如何使用 JavaScript 的 AbortController 有效地取消 fetch 请求、计时器等异步操作,确保代码更整洁、性能更高。
JavaScript AbortController:精通异步操作取消
在现代 Web 开发中,异步操作无处不在。从 API 获取数据、设置计时器以及处理用户交互通常都涉及独立运行且可能持续较长时间的代码。然而,在某些情况下,您需要在这些操作完成之前取消它们。这正是 JavaScript 中的 AbortController
接口发挥作用的地方。它提供了一种简洁高效的方式,向 DOM 操作和其他异步任务发送取消请求的信号。
理解取消操作的必要性
在深入探讨技术细节之前,让我们先理解为什么取消异步操作很重要。请看以下常见场景:
- 用户导航:用户发起一个搜索查询,触发了 API 请求。如果他们在请求完成前迅速导航到另一个页面,那么原始请求就变得无关紧要,应该被取消以避免不必要的网络流量和潜在的副作用。
- 超时管理:你为一个异步操作设置了超时。如果操作在超时到期前完成,你应该取消超时以防止冗余的代码执行。
- 组件卸载:在像 React 或 Vue.js 这样的前端框架中,组件经常会发起异步请求。当组件卸载时,任何与该组件相关的正在进行的请求都应该被取消,以避免内存泄漏和因更新已卸载组件而导致的错误。
- 资源限制:在资源受限的环境中(例如,移动设备、嵌入式系统),取消不必要的操作可以释放宝贵的资源并提高性能。例如,如果用户滚动经过页面的某个部分,可以取消该部分的大图下载。
介绍 AbortController 和 AbortSignal
AbortController
接口旨在解决取消异步操作的问题。它由两个关键部分组成:
- AbortController:该对象管理取消信号。它只有一个方法
abort()
,用于发出取消请求的信号。 - AbortSignal:该对象表示一个操作应该被中止的信号。它与一个
AbortController
相关联,并被传递给需要可取消的异步操作。
基本用法:取消 Fetch 请求
让我们从一个取消 fetch
请求的简单示例开始:
const controller = new AbortController();
const signal = controller.signal;
fetch('https://api.example.com/data', { signal })
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Data:', data);
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Fetch error:', error);
}
});
// To cancel the fetch request:
controller.abort();
说明:
- 我们创建一个
AbortController
实例。 - 我们从
controller
中获取关联的AbortSignal
。 - 我们将
signal
传递给fetch
的选项。 - 如果我们需要取消请求,我们调用
controller.abort()
。 - 在
.catch()
块中,我们检查错误是否为AbortError
。如果是,我们就知道请求已被取消。
处理 AbortError
当调用 controller.abort()
时,fetch
请求将被一个 AbortError
拒绝。在代码中妥善处理这个错误至关重要。否则可能导致未处理的 promise rejection 和意外行为。
这是一个包含错误处理的更健壮的示例:
const controller = new AbortController();
const signal = controller.signal;
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data', { signal });
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
console.log('Data:', data);
return data;
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
return null; // 或者向上抛出错误以供进一步处理
} else {
console.error('Fetch error:', error);
throw error; // 重新抛出错误以供进一步处理
}
}
}
fetchData();
// To cancel the fetch request:
controller.abort();
处理 AbortError 的最佳实践:
- 检查错误名称:始终检查
error.name === 'AbortError'
以确保您正在处理正确的错误类型。 - 返回默认值或重新抛出:根据您的应用程序逻辑,您可能希望返回一个默认值(例如
null
)或重新抛出错误以在调用堆栈的更上层处理。 - 清理资源:如果异步操作分配了任何资源(例如,计时器、事件监听器),请在
AbortError
处理程序中清理它们。
使用 AbortSignal 取消计时器
AbortSignal
也可以用于取消使用 setTimeout
或 setInterval
创建的计时器。这需要一些额外的手动工作,因为内置的计时器函数不直接支持 AbortSignal
。您需要创建一个自定义函数来监听中止信号,并在触发时清除计时器。
function cancellableTimeout(callback, delay, signal) {
let timeoutId;
const timeoutPromise = new Promise((resolve, reject) => {
timeoutId = setTimeout(() => {
resolve(callback());
}, delay);
signal.addEventListener('abort', () => {
clearTimeout(timeoutId);
reject(new Error('Timeout Aborted'));
});
});
return timeoutPromise;
}
const controller = new AbortController();
const signal = controller.signal;
cancellableTimeout(() => {
console.log('Timeout executed');
}, 2000, signal)
.then(() => console.log("Timeout finished successfully"))
.catch(err => console.log(err));
// To cancel the timeout:
controller.abort();
说明:
cancellableTimeout
函数接受一个回调函数、一个延迟时间和一个AbortSignal
作为参数。- 它设置一个
setTimeout
并存储超时 ID。 - 它向
AbortSignal
添加一个事件监听器,用于监听abort
事件。 - 当
abort
事件被触发时,事件监听器会清除超时并拒绝 promise。
取消事件监听器
与计时器类似,您可以使用 AbortSignal
来取消事件监听器。当您想要移除与正在卸载的组件相关联的事件监听器时,这尤其有用。
const controller = new AbortController();
const signal = controller.signal;
const button = document.getElementById('myButton');
button.addEventListener('click', () => {
console.log('Button clicked!');
}, { signal });
// To cancel the event listener:
controller.abort();
说明:
- 我们将
signal
作为一个选项传递给addEventListener
方法。 - 当调用
controller.abort()
时,事件监听器将被自动移除。
在 React 组件中使用 AbortController
在 React 中,您可以使用 AbortController
在组件卸载时取消异步操作。这对于防止内存泄漏和因更新已卸载组件而导致的错误至关重要。以下是使用 useEffect
钩子的示例:
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data', { signal });
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
setData(data);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Fetch error:', error);
}
}
}
fetchData();
return () => {
controller.abort(); // 组件卸载时取消 fetch 请求
};
}, []); // 空依赖数组确保此 effect 仅在挂载时运行一次
return (
{data ? (
Data: {JSON.stringify(data)}
) : (
Loading...
)}
);
}
export default MyComponent;
说明:
- 我们在
useEffect
钩子内部创建了一个AbortController
。 - 我们将
signal
传递给fetch
请求。 - 我们从
useEffect
钩子返回一个清理函数。该函数将在组件卸载时被调用。 - 在清理函数内部,我们调用
controller.abort()
来取消 fetch 请求。
高级用例
链式 AbortSignal
有时,您可能希望将多个 AbortSignal
链接在一起。例如,您可能有一个父组件需要取消其子组件中的操作。您可以通过创建一个新的 AbortController
并将其信号传递给父组件和子组件来实现这一点。
在第三方库中使用 AbortController
如果您正在使用的第三方库不直接支持 AbortSignal
,您可能需要调整您的代码以适应库的取消机制。这可能涉及到将库的异步函数包装在您自己的、处理 AbortSignal
的函数中。
使用 AbortController 的好处
- 提升性能:取消不必要的操作可以减少网络流量、CPU 使用和内存消耗,从而提升性能,尤其是在资源受限的设备上。
- 代码更整洁:
AbortController
提供了一种标准化且优雅的方式来管理取消操作,使您的代码更具可读性和可维护性。 - 防止内存泄漏:取消与已卸载组件关联的异步操作可以防止内存泄漏以及因更新已卸载组件而导致的错误。
- 更好的用户体验:取消不相关的请求可以防止显示过时的信息并减少感知的延迟,从而改善用户体验。
浏览器兼容性
AbortController
在现代浏览器中得到广泛支持,包括 Chrome、Firefox、Safari 和 Edge。您可以在 MDN Web Docs 上查看兼容性表格以获取最新信息。
Polyfills
对于不原生支持 AbortController
的旧版浏览器,您可以使用 polyfill。polyfill 是一段代码,用于在旧版浏览器中提供较新功能。网上有多种 AbortController
polyfill 可用。
结论
AbortController
接口是 JavaScript 中用于管理异步操作的强大工具。通过使用 AbortController
,你可以编写出更整洁、性能更高、更健壮的代码,从而优雅地处理取消操作。无论你是从 API 获取数据、设置计时器,还是管理事件监听器,AbortController
都能帮助你提升 Web 应用程序的整体质量。