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

相关推荐
Captaincc2 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师3 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen4 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒4 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow4 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林8184 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang800784 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明4 小时前
第 6 章 组件进阶
前端·vue.js
江华森4 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端
南青4 小时前
Vue 3 中后台实战:我踩过的 10 个坑和最佳实践
前端