今天介绍一个有用的 JavaScript api AbortController
AbortController
是什么AbortController 接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。你可以使用 AbortController.AbortController() 构造函数创建一个新的 AbortController。使用 AbortSignal 对象可以完成与 DOM 请求的通信
这个 api 简单来说就是可以提供一个能力给我们去提前终止一个 fetch 请求
一个终止 fetch 请求的 demo 如下:
fetchButton.onclick = async () => {
const controller = new AbortController();
// 点击abort button实现终止fetch请求
abortButton.onclick = () => controller.abort();
try {
const r = await fetch('/json', { signal: controller.signal });
const json = await r.json();
} catch (e) {
// 如果fetch请求被终止会抛出一个AbortError的错误
const isUserAbort = (e.name === 'AbortError');
}
};
提前终止后这个请求在 network 面板中的 status 显示为 canceled
在没有AbortController
这个 api 之前,我们是没法去让浏览器提前去终止一个请求的。而有了这个 api 之后,浏览器就能提前终止请求进而节约一些用户带宽。除此之外,这个 api 也能给我们带来一些新的开发模式
下面实例化了一个AbortController
,它的signal
属性就是一个AbortSignal
const controller = new AbortController();
const { signal } = controller;
controller.abort()
去终止它对应的signal
signal
本身是不能被直接终止的。可以将它传递给一些函数调用如 fetch 或者直接监听signal
的状态变化(可以通过signal.aborted
查看signal
的状态或者监听它的abort
事件)一些旧的 DOM api 是不支持AbortSignal
。例如WebScocket
只提供了一个close
方法当我们无需使用时进行关闭。如果要使用AbortSignal
则可以类似以下的封装
function abortableSocket(url, signal) {
const w = new WebSocket(url);
if (signal.aborted) {
w.close(); // signal已经终中止的情况下马上关闭websocket
}
signal.addEventListener('abort', () => w.close());
return w;
}
这个使用也很简单,但是需要注意的是如果signal
已经终止的情况下是不会触发abort
事件,需要我们先进行一个判断是否signal
已经终止
我们经常需要在 js 中处理 dom 的监听和卸载工作。但是下面的例子由于事件监听和卸载传入的函数不是同一个引用时不会生效的
window.addEventListener('resize', () => doSomething());
// 不会生效
window.removeEventListener('resize', () => doSomething());
因此我们经常需要一些额外的代码去维护这个回调函数的引用的一致性。而有了AbortSignal
之后我们就可以有一种的新的方式去实现
const controller = new AbortController();
const { signal } = controller;
window.addEventListener('resize', () => doSomething(), { signal });
controller.abort();
因为addEventListener
也能接收signal
属性的。我们最后只需要调用controller.abort()
,这个controller
的signal
传递的相关事件监听都会被自动相应卸载了
在 JavaScript 中我们可能需要在对象中管理非常复杂的生命周期,如WebSocket
。我们需要执行开启然后执行一系列逻辑后终止。可能我们会写以下代码
const someObject = new SomeObject();
someObject.start();
// 执行一些操作后
someObject.stop();
也可以通过AbortSignal
进行实现
const controller = new AbortController();
const { signal } = controller;
const someObject = new someObject(signal);
// 执行一些操作后
controller.abort();
这能非常清晰地表示这个对象只能被执行一次,只能从开始到结束,而不能反过来。如果它终止了后想再次使用则需要再次创建一个对象
可以在很多地方共享一个signal
。我们无需持有多个SomeObject
的实例。只需要调用controller.abort()
,这些SomeObject
的实例都能被终止掉
如果SomeObject
内部也有调用像fetch
之类的内部 api 只需要把这个signal
继续传递,则fetch
也能被一起终止掉
如下是一个例子。展示了两种 signal 的用法。传递给内置 apifetch
和检查signal
状态执行一些操作
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) {
// 执行复杂操作
}
}
}
我们通常会在useEffect
中进行一些异步 api 调用。借助signal
可以在下一次useEffect
重新调用 api 的时候将前一次的调用终止
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)
}
这些方法当前有可能还没有实现
AbortSignal.timeout(ms)
: 创建一个给定时间后终止的AbortSignal
function abortTimeout(ms) {
const controller = new AbortController();
setTimeout(() => controller.abort(), ms);
return controller.signal;
}
AbortSignal.any(signals)
:创建一个AbortSignal
,如果传入的任一signal
终止了,这个返回的signal
也会被终止function abortAny(signals) {
const controller = new AbortController();
signals.forEach((signal) => {
if (signal.aborted) {
controller.abort();
} else {
signal.addEventListener('abort', () => controller.abort());
}
});
return controller.signal;
}
AbortSignal.throwIfAborted()
:如果signal
本身已经终止了,调用该方法会抛出执行abort(reason)
时指定的 reason 异常;否则只会静默执行 if (signal.aborted) {
throw new Error(...);
}
// becomes
signal.throwIfAborted();
这个方法目前不太容易 polyfill,但是可通过下面的工具函数实现
function throwIfSignalAborted(signal) {
if (signal.aborted) {
throw new Error(...);
}
}
https://whistlr.info/2022/abortcontroller-is-your-friend/