JS `AbortController` 与 `AbortSignal`:优雅地取消 Fetch 请求与异步操作

各位观众老爷们,大家好! 今天咱们来聊聊JavaScript里一对儿好基友:AbortControllerAbortSignal。 别看名字挺唬人,其实它们的作用简单来说就是“终止者”! 专门用来优雅地取消那些“磨磨唧唧”的 Fetch 请求和异步操作。

想象一下,你写了一个前端应用,用户点了搜索按钮,结果服务器半天没反应,用户都泡完了一壶茶了还没出结果。 用户肯定不乐意啊! 他们可能会连续点击搜索按钮,导致一堆请求塞满服务器,最后大家都卡死。 这时候, AbortControllerAbortSignal 就派上用场了。

一、 什么是 AbortControllerAbortSignal

简单来说:

  • AbortController: 是一个控制器,你用它来创建 AbortSignal,并且控制 AbortSignal 的状态。 换句话说,它就是个遥控器。
  • AbortSignal: 是一个信号,你把它传递给那些支持“取消”操作的 API (比如 Fetch),API 会监听这个信号,一旦信号被激活(也就是被“终止”),API 就会停止操作。 这就是个接收指令的接收器。

可以把它们想象成你家的电视遥控器和电视机: AbortController 是遥控器, AbortSignal 是电视机。 你按下遥控器的“关机”按钮 (abort()),电视机 (Fetch) 收到信号后,就乖乖关机了。

二、 如何使用 AbortControllerAbortSignal 取消 Fetch 请求?

咱们先来一个简单的例子:

const controller = new AbortController();
const signal = controller.signal;

fetch('https://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);
  })
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Fetch 请求被取消了!');
    } else {
      console.error('Fetch 请求出错:', error);
    }
  });

// 假设 5 秒后取消请求
setTimeout(() => {
  controller.abort();
  console.log('5 秒到了,取消 Fetch 请求!');
}, 5000);

这段代码做了什么?

  1. 创建了一个 AbortController 实例。
  2. 通过 controller.signal 获取到 AbortSignal 对象。
  3. fetch 函数的配置对象中,将 signal 传递进去。 fetch 函数就开始监听这个信号了。
  4. 设置一个定时器,5 秒后调用 controller.abort() 方法。

controller.abort() 被调用时, AbortSignal 就会发出 "abort" 信号, fetch 函数接收到这个信号后,就会立即停止请求,并且抛出一个 AbortError 异常。

重点:

  • 一定要在 fetchcatch 块中捕获 AbortError 异常,这样才能知道请求是被取消的,而不是因为其他原因失败的。
  • controller.abort() 可以带一个可选的参数 reason,用于描述取消的原因。 例如: controller.abort('请求超时')。 这个 reason 会作为 error.message 传递到 catch 块中。

三、 取消多个 Fetch 请求

一个 AbortController 可以控制多个 AbortSignal,从而取消多个 Fetch 请求。 咱们来试试:

const controller = new AbortController();
const signal = controller.signal;

const fetch1 = fetch('https://example.com/data1', { signal });
const fetch2 = fetch('https://example.com/data2', { signal });

Promise.all([fetch1, fetch2])
  .then(responses => {
    return Promise.all(responses.map(response => response.json()));
  })
  .then(data => {
    console.log('所有数据成功获取:', data);
  })
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('所有 Fetch 请求被取消了!');
    } else {
      console.error('Fetch 请求出错:', error);
    }
  });

// 假设 5 秒后取消所有请求
setTimeout(() => {
  controller.abort();
  console.log('5 秒到了,取消所有 Fetch 请求!');
}, 5000);

在这个例子中,我们创建了两个 fetch 请求,并且都使用了同一个 AbortSignal。 当 controller.abort() 被调用时,两个 fetch 请求都会被取消。

四、 取消其他异步操作

AbortControllerAbortSignal 不仅仅可以用于取消 Fetch 请求,还可以用于取消其他的异步操作,比如 setTimeoutsetIntervalPromise 等。

咱们来看一个取消 setTimeout 的例子:

const controller = new AbortController();
const signal = controller.signal;

const timeoutId = setTimeout(() => {
  console.log('定时器执行了!');
}, 10000);

signal.addEventListener('abort', () => {
  clearTimeout(timeoutId);
  console.log('定时器被取消了!');
});

// 假设 5 秒后取消定时器
setTimeout(() => {
  controller.abort();
  console.log('5 秒到了,取消定时器!');
}, 5000);

这个例子中,我们监听了 AbortSignalabort 事件,当事件触发时,我们就调用 clearTimeout 方法来取消定时器。

五、 AbortSignalaborted 属性和 reason 属性

AbortSignal 对象有两个重要的属性:

  • aborted: 一个只读的布尔值,表示 AbortSignal 是否已经被终止。 如果已经被终止,则为 true,否则为 false
  • reason: 一个只读的字符串或对象,表示终止的原因。 这个值是在调用 controller.abort(reason) 时传递的。 如果没有传递 reason,则为 undefined

咱们来看一个例子:

const controller = new AbortController();
const signal = controller.signal;

controller.abort('请求超时');

console.log('signal.aborted:', signal.aborted); // 输出: true
console.log('signal.reason:', signal.reason);   // 输出: 请求超时

六、 兼容性问题

AbortControllerAbortSignal 是比较新的 API,在一些老版本的浏览器中可能不支持。 不过,可以使用 polyfill 来解决兼容性问题。 例如: abortcontroller-polyfill

七、 使用场景总结

AbortControllerAbortSignal 在以下场景中非常有用:

  • 取消用户触发的请求: 例如,用户点击了搜索按钮,然后又快速地修改了搜索条件,这时候可以取消之前的请求,只发送最新的请求。
  • 处理请求超时: 可以设置一个定时器,如果请求在指定的时间内没有返回,就取消请求。
  • 组件卸载时取消请求: 在 React、Vue 等框架中,当组件卸载时,可以取消组件发起的请求,避免内存泄漏。
  • 实现竞态条件处理: 多个异步操作同时进行,只保留第一个完成的结果,取消其他的操作。

八、 代码示例:React 组件中的应用

import React, { useState, useEffect } from 'react';

function MyComponent() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;

    const fetchData = async () => {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch('https://example.com/api/data', { signal });
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const jsonData = await response.json();
        setData(jsonData);
      } catch (err) {
        if (err.name === 'AbortError') {
          console.log('Fetch 被取消了');
        } else {
          setError(err);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    // 组件卸载时取消请求
    return () => {
      controller.abort();
      console.log('组件卸载,取消 Fetch 请求');
    };
  }, []); //  useEffect 的依赖为空数组,表示只在组件挂载和卸载时执行

  if (loading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  if (data) {
    return <div>Data: {JSON.stringify(data)}</div>;
  }

  return <div>No data yet.</div>;
}

export default MyComponent;

这个 React 组件演示了如何在组件挂载时发起 Fetch 请求,并且在组件卸载时取消请求。 这样可以避免在组件卸载后,请求仍然在执行,导致内存泄漏或者状态更新错误。

九、 总结

AbortControllerAbortSignal 提供了一种优雅的方式来取消 Fetch 请求和异步操作。 它们可以提高应用的性能和用户体验,避免不必要的资源浪费和错误。 在开发中,应该尽可能地使用它们来处理可取消的异步操作。

十、 常见问题解答

问题 回答
AbortController 可以重复使用吗? 不可以。 一个 AbortController 只能被 abort() 一次。 如果需要多次取消请求,需要创建多个 AbortController 实例。
AbortSignal 可以传递给多个 fetch 吗? 可以。 同一个 AbortSignal 可以传递给多个 fetch 请求,当 AbortController.abort() 被调用时,所有使用了该 AbortSignalfetch 请求都会被取消。
如何判断 fetch 请求是否被取消了? catch 块中判断 error.name 是否为 'AbortError'。 如果是,则表示请求被取消了。
AbortController 一定要配合 fetch 使用吗? 不是。 AbortControllerAbortSignal 可以用于取消任何支持 AbortSignal 的异步操作,例如 setTimeoutsetIntervalPromise 等。 只要 API 提供了接收 AbortSignal 的接口,就可以使用它们来取消操作。
reason 参数有什么用? reason 参数可以用来描述取消的原因,方便调试和排查问题。 例如,可以传递一个字符串表示“请求超时”,或者传递一个 Error 对象包含更详细的错误信息。 这个 reason 会作为 error.message 传递到 catch 块中(如果 error 是一个 Error 对象)。

好了,今天的讲座就到这里。 希望大家对 AbortControllerAbortSignal 有了更深入的了解。 祝大家编程愉快,bug 永不相见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注