中文

学习如何使用 JavaScript 的 AbortController 有效地取消 fetch 请求、计时器等异步操作,确保代码更整洁、性能更高。

JavaScript AbortController:精通异步操作取消

在现代 Web 开发中,异步操作无处不在。从 API 获取数据、设置计时器以及处理用户交互通常都涉及独立运行且可能持续较长时间的代码。然而,在某些情况下,您需要在这些操作完成之前取消它们。这正是 JavaScript 中的 AbortController 接口发挥作用的地方。它提供了一种简洁高效的方式,向 DOM 操作和其他异步任务发送取消请求的信号。

理解取消操作的必要性

在深入探讨技术细节之前,让我们先理解为什么取消异步操作很重要。请看以下常见场景:

介绍 AbortController 和 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();

说明:

  1. 我们创建一个 AbortController 实例。
  2. 我们从 controller 中获取关联的 AbortSignal
  3. 我们将 signal 传递给 fetch 的选项。
  4. 如果我们需要取消请求,我们调用 controller.abort()
  5. .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 的最佳实践:

使用 AbortSignal 取消计时器

AbortSignal 也可以用于取消使用 setTimeoutsetInterval 创建的计时器。这需要一些额外的手动工作,因为内置的计时器函数不直接支持 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();

说明:

  1. cancellableTimeout 函数接受一个回调函数、一个延迟时间和一​​个 AbortSignal 作为参数。
  2. 它设置一个 setTimeout 并存储超时 ID。
  3. 它向 AbortSignal 添加一个事件监听器,用于监听 abort 事件。
  4. 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();

说明:

  1. 我们将 signal 作为一个选项传递给 addEventListener 方法。
  2. 当调用 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;

说明:

  1. 我们在 useEffect 钩子内部创建了一个 AbortController
  2. 我们将 signal 传递给 fetch 请求。
  3. 我们从 useEffect 钩子返回一个清理函数。该函数将在组件卸载时被调用。
  4. 在清理函数内部,我们调用 controller.abort() 来取消 fetch 请求。

高级用例

链式 AbortSignal

有时,您可能希望将多个 AbortSignal 链接在一起。例如,您可能有一个父组件需要取消其子组件中的操作。您可以通过创建一个新的 AbortController 并将其信号传递给父组件和子组件来实现这一点。

在第三方库中使用 AbortController

如果您正在使用的第三方库不直接支持 AbortSignal,您可能需要调整您的代码以适应库的取消机制。这可能涉及到将库的异步函数包装在您自己的、处理 AbortSignal 的函数中。

使用 AbortController 的好处

浏览器兼容性

AbortController 在现代浏览器中得到广泛支持,包括 Chrome、Firefox、Safari 和 Edge。您可以在 MDN Web Docs 上查看兼容性表格以获取最新信息。

Polyfills

对于不原生支持 AbortController 的旧版浏览器,您可以使用 polyfill。polyfill 是一段代码,用于在旧版浏览器中提供较新功能。网上有多种 AbortController polyfill 可用。

结论

AbortController 接口是 JavaScript 中用于管理异步操作的强大工具。通过使用 AbortController,你可以编写出更整洁、性能更高、更健壮的代码,从而优雅地处理取消操作。无论你是从 API 获取数据、设置计时器,还是管理事件监听器,AbortController 都能帮助你提升 Web 应用程序的整体质量。

进一步阅读