likes
comments
collection

AbortController是你的朋友(用于取消异步任务等 )

作者站长头像
站长
· 阅读数 40

JS 新特性之一是简陋AbortController,它的AbortSignal. 它支持一些新的开发模式,我将在下面介绍,但首先是:规范演示。

它是AbortController用来提供一个fetch()你可以提前中止的:

这是演示代码的简化版本:

fetchButton.onclick = async () => {
  const controller = new AbortController();

  // wire up abort button
  abortButton.onclick = () => controller.abort();

  try {
    const r = await fetch('/json', { signal: controller.signal });
    const json = await r.json();
    // TODO: do something 🤷
  } catch (e) {
    const isUserAbort = (e.name === 'AbortError');
    // TODO: show err 🧨
    // this will be a DOMException named AbortError if aborted 🎉
  }
};

这个例子很重要,因为它展示了以前不可能AbortController发生的事情:即积极地取消网络请求。浏览器将提前停止获取,可能会节省用户的网络带宽。它也不必是用户发起的:想象你可能Promise.race()有两个不同的网络请求,然后取消丢失的那个。🚙🚗💨

伟大的!虽然这很酷,AbortController而且它生成的信号实际上启用了许多新模式,我将在这里介绍。继续阅读。👀

前奏:控制器与信号

我已经演示了构建一个AbortController. 它提供了一个附加的从属信号,称为AbortSignal

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

为什么这里有两个不同的类?好吧,它们有不同的用途。

  • 控制器让持有者通过 中止其附加的信号controller.abort()

  • 该信号不能直接中止,但您可以将其传递给 like 调用fetch(),或直接监听其中止状态。

    您可以使用 来检查其状态signal.aborted,或为该事件添加事件侦听器"abort"。(fetch()在内部这样做——这只是你的代码需要听它的时候。)

换句话说,被中止的东西不应该能够自行中止,因此它为什么只得到AbortSignal.

用例

中止遗留对象

我们的 DOM API 的一些旧部分实际上并不支持AbortSignal. 一个例子是谦虚的WebSocket,它只有一个.close()你可以在完成后调用的方法。您可能会允许它像这样中止:

function abortableSocket(url, signal) {
  const w = new WebSocket(url);

  if (signal.aborted) {
    w.close();  // already aborted, fail immediately
  }
  signal.addEventListener('abort', () => w.close());

  return w;
}

这很简单,但有一个很大的警告:注意即使它已经AbortSignal中止也不会触发它,所以我们实际上必须检查它是否已经完成,并且在这种情况下立即检查。"abort"**.close()

顺便说一句,在这里创建一个工作并立即取消它有点奇怪WebSocket,但否则可能会破坏与我们的调用者的合同,它期望返回 a WebSocket只是知道它可能会在某个时候中止. 立即是一个有效的“某个点”,所以这对我来说似乎很好!🤣

删除事件处理程序

学习 JS 和 DOM 的一个特别烦人的部分是意识到事件处理程序和函数引用不能以这种方式工作:

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

// later (DON'T DO THIS)
window.removeEventListener('resize', () => doSomething());

…这两个回调是不同的对象,因此 DOM 以其无限的智慧——只是无法静默地删除回调,而不会出现错误。🤦 (我实际上认为这是完全合理的,但它可能会绊倒绝对新手开发者。)

这样做的最终结果是,许多处理事件处理程序的代码只需要保留原始引用,这可能会很痛苦。

你可以看到我要去哪里。

使用AbortSignal,您可以简单地获取为您删除它的信号:

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

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

// later
controller.abort();

就这样,您已经简化了事件处理程序管理!

⚠️ 这在某些较旧的 Chrome 版本和 15 之前的 Safari 上不起作用(并且会静默失败),但随着时间的推移,这将消失。

构造函数模式

如果您在 JavaScript 中封装一些复杂的行为,那么如何管理其生命周期可能并不明显。这对于具有明确起点和终点的代码很重要——假设您的代码执行常规的网络获取,或将某些内容呈现到屏幕上,甚至使用类似的东西WebSocket——您想要开始的所有事情,做一段时间,然后停止。✅➡️🛑

传统上,您可能会编写如下代码:

const someObject = new SomeObject();
someObject.start();

// later
someObject.stop();

这很好,但我们可以通过接受一个来使它更符合人体工程学AbortSignal

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

const someObject = new SomeObject(signal);

// later
controller.abort();

你为什么要这样做?

  1. 限制SomeObject从开始→停止的唯一过渡——永远不会再回到开始。这是固执己见,但我实际上相信它简化了构建这些类型的对象 - 很明显它们是一次性的,并且只是在信号中止时完成。如果您想要另一个 SomeObject,请再次构建它。
  2. AbortSignal你可以从其他地方传递一个共享,并且中止SomeObject不需要你持有 SomeObject——一个很好的例子是,假设有几个功能与启动/停止周期相关联,停止按钮可以调用有效的全局controller.abort()when完成。
  3. 如果SomeObject有类似的内置操作fetch(),你可以简单地传递下去AbortSignal!它所做的一切都可以在外部停止,这是确保它的世界被正确拆除的一种方式。

以下是您可以如何使用它:

export class SomeObject {
  constructor(signal) {
    this.signal = signal;

    // do e.g., an initial fetch
    const p = fetch('/json', { signal });
  }

  doComplexOperation() {
    if (this.signal.aborted) {
      // prevent misuse - don't do something complex after abort
      throw new Error(`thing stopped`);
    }
    for (let i = 0; i < 1_000_000; ++i) {
      // do complex thing a lot 🧠
    }
  }
}

这展示了两种使用信号的方法:一种是将它传递给进一步接受它的内置方法(上面的案例 3.),并且在做一些昂贵的事情之前检查是否允许调用(上面的案例 1.)。

(P)react 钩子中的异步工作

*尽管最近关于你应该在里面做什么useEffect*的一些争论,很明显很多人确实使用它来从网络中获取。这很好,但典型的模式似乎是让回调async起作用。

这基本上是赌博。🎲

为什么?好吧,因为如果你的效果在它再次被触发之前没有完成,你不会发现 - 效果只是并行运行。像这样的东西:

function FooComponent({ something }) {
  useEffect(async () => {
    const j = await fetch(url + something);
    // do something with J
  }, [something]);

return <>...<>;
}

相反,您应该做的是创建一个控制器,每当下一次useEffect调用运行时您就中止它:

function FooComponent({ something }) {
  useEffect(() => {
    const controller = new AbortController();
    const { signal } = controller;

    const p = (async () => {
      // !!! actual work goes here
      const j = await fetch(url + something);
      // do something with J
    })();

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

  return <>...<>;
}

现在这是一个非常简化的版本,需要您包装您的电话。您可能想考虑编写自己的钩子(例如,useEffectAsync),或使用库来帮助您。

但是,请记住,在 hook-land 中,您在第一次调用可以访问的内容的生命周期await确实不清楚——您的代码在技术上引用了之前的运行。设置状态之类的事情往往很好,但获取状态是行不通的:

function AsyncDemoComponent() {
  const [value, setValue] = useState(0);

  useEffectAsync(async (signal) => {
    await new Promise((r) => setTimeout(r, 1000));

    // What is "v" here?
    // It's always going to be 0, the initial value, even if the button
    // below was pressed.
  }, []);

  return <button onClick={() => setValue((v) => v + 1)}>Increment</button>
}

无论如何,这是关于 React 生命周期陷阱的另一篇文章。

可能存在或不存在的助手

在您阅读这篇文章时,有一些助手可能可用也可能不可用。我主要演示了非常简单的 使用AbortController,包括自己中止它。

  • AbortSignal.timeout(ms) :这将创建一个独奏AbortSignal,将在给定的超时后自动中止。如果您需要,这很容易自己创建:
function abortTimeout(ms) {
  const controller = new AbortController();
  setTimeout(() => controller.abort(), ms);
  return controller.signal;
}
  • AbortSignal.any(signals) :如果任何传递的信号被中止,这将创建一个中止的信号。同样,您可以自己构造它——但请注意,如果您不传递任何信号,则派生信号将永远不会中止:
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() :这是一个助手,AbortSignal如果中止它会抛出一个错误——阻止你不断地检查它。你像这样使用它:
  if (signal.aborted) {
    throw new Error(...);
  }
  // becomes
  signal.throwIfAborted();

这更难填充,但你可以编写一个帮助程序,如:

function throwIfSignalAborted(signal) {
  if (signal.aborted) {
    throw new Error(...);
  }
}