尽情使用 AbortController(Don't Sleep on AbortController)
今天,我想讨论一个名叫 AbortController
的 JavaScript 标准 APIs。也许你想要睡觉,还是接着读…
AbortController
?
AbortController
是 JavaScript 中用于中断任何事的全局类。使用方法如下:
const controller = new AbortController()
controller.signal
controller.abort()
一旦创建一个 AbortController
实例,就会得到两件事:
signal
属性,它是AbortSignal
的一个实例。它可以填充到提供响应中断事件的 API 中,然后执行中断。举例来说,填充到fetch
请求中可以中断请求;.abort()
方法,该方法一旦执行,就会触发signal
上的中断事件。
到目前为止一切顺利。但实际的中止逻辑在哪里呢?这正是它的美妙之处 —— 由使用者定义。中止处理的核心在于监听中止事件,并以适合当前逻辑的方式实现中止:
controller.signal.addEventListener('abort', () => {
// Implement the abort logic.
})
下面我们探究在 JavaScript 标准中提供的 AbortSignal
。
使用
事件监听
可以在事件监听中添加中断属性 signal
,当中断发生时它会自动移除事件监听。
const controller = new AbortController()
window.addEventListener('resize', listener, { signal: controller.signal })
controller.abort()
执行 controller.abort()
会移除 window 上的 resize
事件监听。这是非常优雅的卸载事件监听方式,因为不再只能使用固定的卸载事件监听函数 .removeEventListener()
。
// 1
function listener() {}
window.addEventListener('resize', listener)
window.removeEventListener('resize', listener)
// 2
const controller = new AbortController()
window.addEventListener('resize', () => {}, { signal: controller.signal })
controller.abort()
如果应用程序的不同部分负责移除监听器,将 AbortController
实例传递下去会更加方便。
对我来说,一个很棒的“顿悟”时刻是,意识到你可以使用单个 signal
来移除多个事件监听器!
function eventListener() {
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()
}
}, [])
}
在上面的例子中,我使用 React useEffect()
hook 添加一串不同逻辑的事件监听。注意在 clean up 函数中,我只需要执行一次 controller.abort()
就移除了所有的事件监听。
fetch 请求
fetch()
函数同样支持 AbortSignal
。一旦触发 abort
事件信号,fetch()
函数代表的请求 promise 的状态将会变为 reject,中断请求的继续。
function uploadFile(file) {
const controller = new AbortController()
// Provide the abort signal to this fetch request
// so it can be aborted anytime be calling `controller.abort()`.
const response = fetch('/upload', {
method: 'POST',
body: file,
signal: controller.signal,
})
return { response, controller }
}
在这里,uploadFile
函数启动一个 POST /upload
请求,返回与上传关联的 response
promise,同时也添加 controller
中断引用。如果需要取消上传操作的话这是一个重要的点,例如,当用户点击“取消”按钮。
Node.js 中由
http
模块发出的请求同样支持signal
属性!
AbortSignal
类还提供了一些静态方法,以简化 JavaScript 中的请求处理。
AbortSignal.timeout
你可以使用 AbortSignal.timeout()
静态方法作为简写,创建一个在特定超时时间后触发中止事件的信号。如果你只想在请求超时后取消请求,就无需创建 AbortController
:
fetch(url, {
// 如果这个请求超过三秒将会自动取消
signal: AbortSignal.timeout(3000),
})
AbortSignal.any
类似于使用 Promise.race()
来处理多个 Promise 以先到先服务的方式,你可以利用 AbortSignal.any()
静态方法将多个中止信号组合成一个。
const publicController = new AbortController()
const internalController = new AbortController()
channel.addEventListener('message', handleMessage, {
signal: AbortSignal.any([publicController.signal, internalController.signal]),
})
在上面的示例中,我引入了两个中止控制器。公共控制器暴露给代码的消费者,允许他们触发中止,这样消息 message
事件监听器就会被移除。然而,内部控制器允许我在不干扰公共中止控制器的情况下移除该监听器。
如果提供给 AbortSignal.any()
的任何中止信号触发了中止事件,那么父信号也将触发中止事件。在此之后的任何其他中止事件将被忽略。
流 streams
可以使用 AbortController
和 AbortSignal
中止流。
const stream = new WritableStream({
write(chunk, controller) {
controller.signal.addEventListener('abort', () => {
// 流中断后做些什么...
})
},
})
const writer = stream.getWriter()
await writer.abort()
控制器 WritableStream
暴露出 signal
属性,该属性与 AbortSignal
相同。于是,我可以执行 writer.abort()
,这将触发流的 write()
方法中 controller.signal
订阅的中断事件。
使每件事可中断
我最喜欢的关于 AbortController
API 的部分是它非常灵活。如此灵活,以至于你可以让任何逻辑变得可中止!
拥有这样的超能力,不仅可以自己提供更好的体验,还可以增强你使用那些不支持中止/取消的第三方库的方式。事实上,我们就来做这个。
让我们将 AbortController
添加到 Drizzle ORM 的事务中,这样我们就能够一次取消多个事务。
import { TransactionRollbackError } from 'drizzle-orm'
function makeCancelableTransaction(db) {
return (callback, options = {}) => {
return db.transaction((tx) => {
return new Promise((resolve, reject) => {
// Rollback this transaction if the abort event is dispatched.
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
错误来拒绝事务承诺,以回滚整个事务(这相当于调用 tx.rollback()
,并抛出相同的错误)。
现在,让我们在 Drizzle 中使用它。
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 },
)
我调用 makeCancelableTransaction()
工具函数,并传入 db
实例,以创建一个自定义的可中止事务 transaction
。从此以后,我可以像在 Drizzle 中一样正常使用我的自定义事务 transaction
,执行多个数据库操作,同时也可以提供一个中止 signal
,以便一次性取消所有操作。
中止错误行为
每个中止事件都伴随着中止的原因。这为你提供了更多的自定义可能性,因为你可以对不同的中止原因做出不同的反应。
中止原因是 controller.abort()
方法的一个可选参数。你可以在任何 AbortSignal
实例的 reason
属性中访问中止原因。
const controller = new AbortController()
controller.signal.addEventListener('abort', () => {
console.log(controller.signal.reason) // "user cancellation"
})
// 给这个中止提供一个自定义原因
controller.abort('user cancellation')
reason
参数可以是任何 JavaScript 值,因此你可以传递字符串、错误甚至对象。
总结
如果你正在创建 JavaScript 库,其中中止或取消操作是合理的,我强烈建议你使用 AbortController
API。它非常出色!如果你在构建应用程序,当需要取消请求、移除事件监听器、中止流,或使任何逻辑变得可中止时,你都可以有效地利用中止控制器。
文章由 吳文俊 翻译,原文引用 Don’t Sleep on AbortController