[译] 不要忽视 AbortController.md

今天,我想介绍一个你可能没怎么使用的标准 JavaScript API,它叫做 AbortController。

什么是 AbortController?

AbortController 是 JavaScript 中的一个全局类,你可以用它来终止任何事情!

下面是使用方法:

js 复制代码
const controller = new AbortController()

controller.signal
controller.abort()

创建 AbortController 实例后,你会得到以下两个东西:

  • signal 属性,它是 AbortSignal 的一个实例。AbortSignal 是可插拔的,你可以将其提供给任何 API,以对终止事件做出响应,并相应地实现逻辑。例如,将其提供给 fetch() 请求将终止该请求;
  • .abort() 方法,调用它时会触发 signal 上的终止事件。它还会更新 signal,将其标记为终止。

到目前为止一切顺利。但实际的终止逻辑在哪里呢?这就是美妙之处 - 它是由消费者定义的。终止处理就是监听 abort 事件,并以符合相关逻辑的方式实现终止:

ts 复制代码
controller.signal.addEventListener('abort', () => {
  // 执行终止逻辑。
})

让我们探索一下支持 AbortSignal 的标准 JavaScript API。

使用方法

事件监听

你可以在添加监听事件时提供 signal,以便在调用终止方法时自动移除该事件监听。

js 复制代码
const controller = new AbortController()

window.addEventListener('resize', listener, { signal: controller.signal })

controller.abort()

调用 controller.abort() 将移除 window 中的 resize 事件监听。这是一种优雅的事件监听处理方式,因为你无需定义具名的监听函数以便提供给 .removeEventListener() 方法。

js 复制代码
// const listener = () => {}
// window.addEventListener('resize', listener)
// window.removeEventListener('resize', listener)

const controller = new AbortController()
window.addEventListener('resize', () => {}, { signal: controller.signal })
controller.abort()

如果应用中有不同模块都需要有移除事件监听的能力,那么传递 AbortController 实例即可将移除监听的能力分发。

当我意识到可以使用单个 signal 移除多个事件监听器时,我有了一个伟大的 "啊哈" 时刻!

js 复制代码
useEffect(() => {
  const controller = new AbortController()

  window.addEventListener('resize', handleResize, {
    signal: controller.signal,
  })
  window.addEventListener('hashchange', handleHashChange, {
    signal: controller.signal,
  })
  window.addEventListener('storage', handleStorageChange, {
    signal: controller.signal,
  })

  return () => {
    // 调用 .abort() 会移除所有事件监听器
    // 与 controller.signal 关联。消失!
    controller.abort()
  }
}, [])

在上面的示例中,我添加了一个 useEffect() Hook,它引入了一系列具有不同目的和逻辑的事件监听。请注意,在清理函数中,我只需调用一次 controller.abort(),就能移除所有添加的事件监听。真不错!

Fetch 请求

fetch() 函数也支持 AbortSignal。一旦 signal 上的终止事件被触发,fetch() 函数返回的 promise 将被拒绝,从而终止待处理的请求。

js 复制代码
function uploadFile(file: File) {
  const controller = new AbortController()

  // 提供一个 signal 给 fetch 请求
  // 所以它可以随时使用 controller.abort() 终止请求
  const response = fetch('/upload', {
    method: 'POST',
    body: file,
    signal: controller.signal,
  })

  return { response, controller }
}

在这里,uploadFile() 函数初始化了一个 POST /upload 请求,函数返回了相关的响应 promise,但同时也返回了一个 AbortController 实例,以便在任何时候终止该请求。例如,当用户点击 "取消" 按钮时,我可以直接使用 controller 终止请求,这将非常方便。

Node.js 中 http 模块的请求也支持 signal 属性!

AbortSignal 类还提供了一些静态方法,以简化 JavaScript 中的请求处理。

AbortSignal.timeout

你可以使用 AbortSignal.timeout() 静态方法快速创建一个 signal,该实例在一定时间后将自动发送终止事件。如果你只想在请求超过一定时间后取消请求,则无需创建 AbortController,可以直接使用 AbortSignal.timeout()

js 复制代码
fetch(url, {
  // 如果 3s 后请求还未完成,请求就会自动取消。
  signal: AbortSignal.timeout(3000),
})
AbortSignal.any

与使用 Promise.race() 以先到先得的方式处理多个 promise 类似,你也可以使用 AbortSignal.any() 静态方法将多个 signal 合并为一个 signal

js 复制代码
const publicController = new AbortController()
const internalController = new AbortController()

channel.addEventListener('message', handleMessage, {
  signal: AbortSignal.any([publicController.signal, internalController.signal]),
})

在上面的示例中,我引入了两个终止控制器。公共控制器暴露给外部,允许他们触发终止,从而移除消息事件监听。而内部控制器则允许我在不影响公共终止控制器的情况下移除该事件监听。

如果提供给 AbortSignal.any()signal 中的任何一个发出了终止事件,则该父级 signal 也将发出终止事件,并且随后的任何其他终止事件都将被忽略。

Streams

你也可以使用 AbortControllerAbortSignal 取消流。

js 复制代码
const stream = new WritableStream({
  write(chunk, controller) {
    controller.signal.addEventListener('abort', () => {
      // 在这里处理流的取消
    })
  },
})

const writer = stream.getWriter()
await writer.abort()

WritableStream 提供了 AbortSignal 实例的相关属性和方法。这样,我就可以调用 writer.abort(),从而在流中的 write() 方法中触发 controller.signal 上的终止事件。

让任何功能都可以终止

我最喜欢 AbortController API 的一点就是,它的用途非常广泛,你可以让任何逻辑变得可终止!

有了这样一种唾手可得的超级能力,你不仅可以给自己提供更好的体验,还可以增强第三方库的能力,让它们也支持终止/取消的能力。接下来,我们就来做这件事。

让我们将 AbortController 添加到 Drizzle ORM 事务中,这样就能一次性取消多个事务。

js 复制代码
import { TransactionRollbackError } from 'drizzle-orm'

function makeCancelableTransaction(db) {
  return (callback, options = {}) => {
    return db.transaction((tx) => {
      return new Promise((resolve, reject) => {
        // 如果触发了终止事件,则回滚该事务。
        options.signal?.addEventListener('abort', async () => {
          reject(new TransactionRollbackError())
        })

        return Promise.resolve(callback.call(this, tx)).then(resolve, reject)
      })
    })
  }
}

makeCancelableTransaction() 函数接受一个数据库实例,并返回一个高阶事务处理函数,该函数现在接受一个 signal 作为参数。

为了感知终止的发生,我在 signal 实例上添加了 abort 的事件监听。只要终止事件发生,即调用 controller.abort() 时,该事件监听器就会被调用。因此,当这种情况发生时,我就可以用 TransactionRollbackError 错误拒绝事务 promise,从而回滚整个事务(这与调用 tx.rollback() 引发的错误相同)。

现在,让我们将它与 Drizzle 配合使用。

js 复制代码
const db = drizzle(options)

const controller = new AbortController()
const transaction = makeCancelableTransaction(db)

await transaction(
  async (tx) => {
    await tx
      .update(accounts)
      .set({ balance: sql`${accounts.balance} - 100.00` })
      .where(eq(users.name, 'Dan'))
    await tx
      .update(accounts)
      .set({ balance: sql`${accounts.balance} + 100.00` })
      .where(eq(users.name, 'Andrew'))
  },
  { signal: controller.signal }
)

我使用 db 实例调用 makeCancelableTransaction() 函数,创建了一个自定义的可终止事务。从现在起,我就可以使用我的自定义事务执行多个数据库操作,就和正常使用 Drizzle 一样,但我也可以为它提供一个终止 signal,一次性取消所有操作。

abort 中的错误处理

每个终止事件都会附带终止原因。这将带来更多的自定义功能,因为你可以对不同的终止原因做出不同的处理。

终止原因是 controller.abort() 方法的可选参数。你可以在任何 AbortSignal 实例的 reason 属性中访问终止原因。

js 复制代码
const controller = new AbortController()

controller.signal.addEventListener('abort', () => {
  console.log(controller.signal.reason) // "user cancellation"
})

// 提供一个自定义的终止原因
controller.abort('user cancellation')

reason 参数可以是任何 JavaScript 值,因此可以传递字符串、Error 甚至对象。

总结

如果你的 JavaScript 库需要终止或取消操作,我强烈建议你使用 AbortController API。它真是不可思议!如果你正在构建应用,那么当你需要取消请求、移除事件监听器、终止流或让任何逻辑变得可终止,你可以利用 AbortController 来达到最佳效果。

后记

特别感谢 Oleg Isonen 对本文的校对!

相关推荐
yuguo.im几秒前
91 行代码实现一个打飞机游戏(HTML5 Canvas 版)
前端·游戏·html5·打飞机
前端小D8 分钟前
面向对象编程
开发语言·javascript
从文处安20 分钟前
「前端何去何从」一直写 Vue ,为何要在 AI 时代去学 React「2」?
前端·react.js
掘金者阿豪21 分钟前
深入解读OpenClaw配置文件:一个现代化AI网关的全景洞察
前端
进击的尘埃22 分钟前
GPU 合成层炸了,页面白屏——从 will-change 滥用聊到层爆炸的治理
javascript
葡萄城技术团队22 分钟前
Playwright 官方推荐的 Fixture 模式,为什么大厂架构师却在偷偷弃用?
前端
newbe3652424 分钟前
ImgBin CLI 工具设计:HagiCode 图片资产管理方案
前端·后端
bluceli26 分钟前
CSS Scroll Snap:打造丝滑滚动体验的利器
前端·css
www_stdio27 分钟前
深入理解 React Fiber 与浏览器事件循环:从性能瓶颈到调度机制
前端·react.js·面试