介绍

AbortController 接口表示一个控制器对象,用来中止一个或多个请求。使用 AbortController.signal 属性可以获取其关联的 AbortSignal 对象的引用。

AbortSignal 对象表示一个信号,可以完成与 DOM 请求的通信。

方法和属性

AbortController.signal

  • 返回一个 AbortSignal 对象实例,用来传递中止信号。

AbortController.abort()

  • 中止它的 signal 对应的请求。

AbortSignal.aborted

  • 以 Boolean 表示与之通信的请求是否被终止(true)或未终止(false)。

AbortSignal.reason

  • 一旦信号中止,提供中止原因。

AbortSignal.throwIfAborted()
如果信号已中止,则会抛出信号的中止原因;否则它什么也不做。

静态方法 AbortSignal.abort(reason)

  • 返回一个已设置为 aborted(并且不会触发 abort 事件)的 AbortSignal。该方法的目的类似于 Promise.reject。如果未指定 reason,则将原因设置为 DOMException

静态方法 AbortSignal.timeout(time)

  • 返回一个 AbortSignal,它将在指定时间后自动中止。

事件属性 AbortSignal.onabort

  • abort 事件触发时,即当信号正在与之通信的 DOM 请求被中止时调用。

其他

  • AbortSignal 接口从其父接口 EventTarget 继承属性和方法。

使用场景

  1. 下面的示例我们通过封装 Fetch API 来下载一段视频。当一个 fetch 请求初始化时,我们把 AbortSignal 作为一个选项 { signal } 传递到到请求对象,然后就可以通过调用 AbortController.abort() 来中止请求。当 abort() 方法被调用时,这个 fetch 请求将 reject 一个名为 AbortErrorDOMException
const controller = new AbortController();
const signal = controller.signal;

downloadBtn.addEventListener('click', fetchVideo);

abortBtn.addEventListener('click', function() {
  controller.abort('Download aborted');
});

function fetchVideo() {
  fetch(url, { signal }).then(function(response) {
    //...
  }).catch(function(e) {
    reports.textContent = 'Download error: ' + e.message;
  });
}
  1. 下面的示例封装了一个 WebScocket,并通过 AbortSignal 来关闭其连接。
function abortableSocket(url, signal) {
  const w = new WebSocket(url);

  if (signal.aborted) {
    w.close();  // signal 已经终止的情况下马上关闭 websocket
  }
  signal.addEventListener('abort', () => w.close());

  return w;
}
  1. 我们经常需要在 js 中处理 dom 的监听和卸载工作。但是下面的例子由于事件监听和卸载传入的函数不是同一个引用时不会生效的。
window.addEventListener('resize', () => doSomething());

// 不会生效
window.removeEventListener('resize', () => doSomething());

因此我们经常需要一些额外的代码去维护这个回调函数的引用的一致性。有了 AbortSignal 之后我们就可以通过 addEventListener 第三个参数我们可以传递一个 signal,当 signal 中止时,事件监听将被移除。

const controller = new AbortController();
const { signal } = controller;

window.addEventListener('resize', () => doSomething(), { signal });

controller.abort();
  1. 在 JavaScript 中我们可能需要在对象中管理非常复杂的生命周期,比如先执行开启然后执行一系列逻辑后终止。
const someObject = new SomeObject();
someObject.start();

// 执行一些操作后
someObject.stop();

这种情况可以通过 AbortSignal 进行实现。

const controller = new AbortController();

const someObject = new SomeObject(controller.signal);

// 执行一些操作后
controller.abort();

通过 AbortSignal 实现的好处是:

  • 这能非常清晰地表示这个对象只能被执行一次,只能从开始到结束,而不能反过来。如果它终止了后想再次使用则需要再次创建一个对象。
  • 可以在很多地方共享一个 signal。我们无需持有多个 SomeObject 的实例。只需要调用 controller.abort(),这些 SomeObject 的实例都能被终止掉。
  • 如果 SomeObject 内部也有调用像 fetch 之类的内部 api 只需要把这个 signal 继续传递,则 fetch 也能被一起终止掉。例如以下代码:
export class SomeObject {
  constructor(signal) {
    this.signal = signal;

    // 一些请求
    const p = fetch('/json', { signal });
  }

  // 执行复杂操作
  doComplexOperation() {
    if (this.signal.aborted) {
      throw new Error(`thing stopped`);
    }
    for (let i = 0; i < 1_000_000; ++i) {
      // ...
    }
  }
}
  1. 我们通常会在 React Hook useEffect 中进行一些异步 api 调用。可以借助 signal 在下一次 useEffect 重新调用时将前一次的调用终止。
function FooComponent({ something }) {
  useEffect(() => {
    const controller = new AbortController();
    const { signal } = controller;

    const p = (async () => {
      const j = await fetch(url + something, { signal });
    })();

    return () => controller.abort();
  }, [something]);

  return <>...<>;
}

也可以封装一个 useEffectAsync 的 Hook。

function useEffectAsync(cb, dependence) {
   const controller = new AbortController();
   const { signal } = controller;
   useEffect(() => {
     cb(signal);
     return () => controller.abort();
   }, dependence)
}

总结

AbortControllerAbortSignal 提供了一些用来中止请求和事件的接口,帮助我们提前终止请求进而节约一些资源,也给我们带来了一些新的开发模式。