- 原文地址:Don't Sleep on AbortController
- 原文作者:kettanaito
今天,我想介绍一个你可能没怎么使用的标准 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
你也可以使用 AbortController
和 AbortSignal
取消流。
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 对本文的校对!