[译] 不要忽视 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 对本文的校对!

相关推荐
别怪我很水3 分钟前
Vue element admin 浏览器本地存储 localStorage、useStorage
前端·javascript·vue.js
手揽回忆怎么睡4 分钟前
Ubuntu 22.04 64位安装MinIO
linux·前端·ubuntu
铁皮饭盒7 分钟前
网页端OCR, 加载6mb大小模型, 又快又准, 百度这次真香
前端·后端
Web极客码23 分钟前
如何在WordPress中添加动态内容
服务器·前端·wordpress
云浪34 分钟前
给 AI Agent 加上语音交互:ASR + 流式 TTS 实战
前端·人工智能·后端
晓得迷路了1 小时前
栗子前端技术周刊第 140 期 - pnpm、Node 新 API 文档网站、Oxlint...
前端·javascript·node.js
烬羽1 小时前
一个队列,怎么让滑动窗口从 O(nk) 变 O(n)?单调队列彻底搞懂
javascript·数据结构·算法
weixin_3823952310 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__10 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.11 小时前
Vue3 Props 属性
前端·javascript·vue.js