使用 AbortController 取消 Fetch 请求和事件监听

2,574 阅读2分钟

前言

AbortController 是一个控制器对象,你可以通过 new 构造函数的形式返回一个 AbortSignal 对象:

const controller = new AbortController();
  • 它有一个方法 abort(),用于触发 abort 事件
  • 和单个属性 signal,可以在该属性上设置 abort 事件监听
controller.signal.onabort = () => {};

controller.signal.addEventListener("abort", () => {});

controller.abort() 被调用时:

  • controller.signal 就会触发 abort 事件
  • controller.signal.aborted 属性从 false 变为 true

了解前置知识后,看一下实际应用的场景。

取消 Fetch 请求

在没有 Fetch API 之前,一般通过 XMLHttpRequest 来实现数据更新,当我们发出请求后想临时取消,可以调用 XMLHttpRequest.abort() 方法来终止该请求。

有了 AbortController,同样可以实现取消 Fetch 请求。

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

fetch("/", { signal }).catch(err => {
  console.log(err); // DOMException: The user aborted a request.
  console.log("aborted:", signal.aborted); // aborted: true
});

controller.abort();

移除事件监听

以往,我们通过 EventTarget.addEventListener() 来为一个 DOM、document 或 window 添加事件监听。

它的函数签名如下:

target.addEventListener(type, listener);
target.addEventListener(type, listener, options);
target.addEventListener(type, listener, useCapture);

options 是一个可选参数对象,它包含了以下属性来决定事件触发的行为:

  • capture: boolean,如果是 true,表示 listener 会在指定 type 事件类型的 捕获 阶段触发。false 则为 冒泡 阶段
  • once: boolean,表示 listener 在添加之后最多只调用一次。如果是 true, listener 会在其被调用之后自动移除
  • passive: boolean,设置为 true 时,表示 listener 永远不会调用 preventDefault(),用于优化页面的滚动性能

当不再需要事件监听时,调用 EventTarget.removeEventListener() 来移除事件监听,但要保证 参数和添加监听时是一致的,即 type 事件类型必须是同一字符串,listener 回调函数必须是同一个函数引用,options 对象必须属性相同。

const btn = document.getElementsByTagName("button")[0];

btn.addEventListener("click", () => console.log("button clicked"), {
  capture: true,
});

// Doesn't work, because has different callback functions
btn.removeEventListener("click", () => console.log("button clicked"), {
  capture: true,
});

在 Chrome 88 发布时,添加了一个 AbortSignal in addEventListener 的新特性,它允许 AbortController 实例的 signal 属性传入 addEventListener() 的 options 对象,一旦调用实例的 abort() 方法,就会移除对应的事件监听。

这意味着开发者摆脱了调用 removeEventListener() 时繁琐的参数困境,只需使用 abort() 方法,既减少了代码量,又一目了然。

下面的代码实现了 "click once" 的效果,实测(Chrome 90)运行成功。

const controller = new AbortController();
const signal = controller.signal;
const btn = document.getElementsByTagName("button")[0];

const handle = () => {
  console.log("button clicked");
  controller.abort();
};

btn.addEventListener("click", handle, {
  capture: true,
  signal,
});

// equals to
// btn.addEventListener("click", handle, { once: true });

其他应用场景

取消定时器 - 初级版:

function timeout(duration, signal) {
  return new Promise((resolve, reject) => {
    const handle = setTimeout(resolve, duration);
    signal.onabort = () => {
      clearTimeout(handle);
      reject(new Error("The timeout was aborted"));
    };
  });
}

const controller = new AbortController();

timeout(10000, controller.signal).catch(err => console.log(err));

controller.abort();

取消定时器 - 高级版:

需要 Node.js v16 的支持

import { setTimeout } from "timers/promises";

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

setTimeout(1000, "foobar", { signal })
  .then(console.log)
  .catch(err => {
    if (err.name === "AbortError") console.log("The timeout was aborted");
  });

ac.abort();

Reference