专栏名称: 前端早读课
我们关注前端,产品体验设计,更关注前端同行的成长。 每天清晨五点早读,四万+同行相伴成长。
目录
相关文章推荐
奇舞精选  ·  Chrome 129:正式推出原生的 ... ·  14 小时前  
前端大全  ·  这个 JavaScript API ... ·  2 天前  
前端大全  ·  尤雨溪成立新公司VoidZero:声称打造下 ... ·  5 天前  
奇舞精选  ·  即将出现在CSS中的11个新特性与功能 ·  2 天前  
奇舞精选  ·  即将出现在CSS中的11个新特性与功能 ·  2 天前  
前端早读课  ·  【第3387期】多种前端框架SSR性能大比拼 ·  3 天前  
51好读  ›  专栏  ›  前端早读课

【第3389期】不要忽视 AbortController

前端早读课  · 公众号  · 前端  · 2024-10-10 08:00

正文

前言

介绍了 JavaScript 中的 AbortController API,它允许开发者中止任何操作,如 HTTP 请求、事件监听器、数据流等,以及如何使用 AbortSignal 来实现超时、取消和流控制,以及如何在自定义逻辑中实现取消能力。今日前端早读课文章由 @飘飘翻译分享。

译文从这开始~~

今天,我想谈谈一个你可能忽略了的标准 JavaScript API。它叫做 AbortController 。

AbortController 是什么?

AbortController 是 JavaScript 中的全局类,可用于终止任何操作。

【早阅】VoidZero Inc.:下一代 JavaScript 工具链

这是使用方法:

 const controller = new AbortController()

controller.signal
controller.abort()

一旦创建了控制器实例,你会得到两样东西:

  • signal 属性,是 AbortSignal 的一个实例。这是一个可插拔的部分,您可以将其提供给任何 API 以响应中断事件,并相应地实现它。例如,将其提供给 fetch() 请求将中止请求;

  • 调用 .abort() 方法时,会触发 signal 的 abort 事件。它还会更新 signal,将其标记为已取消。

到目前为止一切顺利。但实际的中止逻辑在哪里?这就是美妙之处 —— 它由使用者定义。中止处理就是监听 abort 事件,并以适合特定逻辑的方式实现终止操作。

 controller.signal.addEventListener('abort', () => {
// Implement the abort logic.
})

让我们一起探索一下那些内置支持 AbortSignal 的标准 JavaScript API。

使用方法

事件监听器

当为某个事件添加监听器时,可以提供一个 abort signal ,以便在 abort 发生时自动移除该监听器。

 const controller = new AbortController()

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

controller.abort()

调用 controller.abort() 将从窗口中移除 resize 监听器。这是一种极其优雅的处理事件监听器的方式,因为你不再需要抽象监听器函数,只需将其提供给 .removeEventListener() 即可 。

 // const listener = () => {}
// window.addEventListener('resize', listener)
// window.removeEventListener('resize', listener)

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

如果应用程序的其他部分负责移除监听器,那么使用匿名内部类作为监听器会更加方便。

让我感到非常惊喜的一件事情是,我发现可以用一个简单的 removeEventListener() 方法来移除多个事件监听器!

 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 () => {
// Calling `.abort()` removes ALL event listeners
// associated with `controller.signal`. Gone!
controller.abort()
}
}, [])

在上面的例子中,我在 React 中添加了一个 useEffect() 钩子,引入了各种具有不同目的和逻辑的事件监听器。请注意,在清理函数中,通过一次调用 controller.abort() 来移除所有添加的监听器。真棒!

【第3312期】 JavaScript 如何中止Promise

获取请求

fetch() 函数也支持 AbortSignal !当 signal 上的 abort 事件被触发时, fetch() 函数返回的请求承诺将被拒绝,中止正在进行的请求。

 function uploadFile(file: 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() 的静态方法作为一种简便的方法来创建一个 signal,会在经过一定超时时间后触发 abort 事件。如果你只想在请求超时后取消请求,就不需要创建 AbortController 。

 fetch(url, {
// Abort this request automatically if it takes
// more than 3000ms to complete.
signal: AbortSignal.timeout(3000),
})

AbortSignal.any

类似于如何使用 Promise.race() 来按先到先处理多个 Promise,你可以利用 AbortSignal.any() 静态方法将多个取消 signal 组合成一个。

 const publicController = new AbortController()
const internalController = new AbortController()

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

在上面的例子中,我引入了两个中止控制器。公共的控制器暴露给我的代码的使用者,允许他们触发中止,从而导致 message 事件监听器被移除。然而,内部的控制器则允许我在不干扰公共中止控制器的情况下,也移除那个监听器。

【第3234期】fetchLater:JS全新API支持关闭页面时安全发送网络请求

如果向 AbortSignal.any() 提供的任何终止信号触发了终止事件,那么这个父信号也将发送中止事件。此后的任何其他终止事件将被忽略。

Streams

你可以使用 AbortController 和 AbortSignal 来取消流。

 const stream = new WritableStream({
write(chunk, controller) {
controller.signal.addEventListener('abort', () => {
// Handle stream abort here.
})
},
})

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

这个 WritableStream 控制器暴露了 signal 属性,它与旧的 AbortSignal 属性相同。这样,我就可以调用 writer.abort() ,这将向上传递到 controller.signal 的 write() 方法中的取消事件。

使任何事情都能被中止

我最喜欢的关于 AbortController API 的部分是它极其灵活。灵活到你可以教给它任何逻辑,让它可以被中断!

拥有这样的超级能力,不仅可以自己打造更好的体验,还可以提升对不支持中断 / 取消的第三方库的使用效果。事实上,我们现在就来做这件事。

让我们在 Drizzle ORM 事务中添加 AbortController ,以便一次取消多个事务。

 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() 函数接受一个数据库实例作为参数,并返回一个高级事务函数,该函数现在接受一个 abort signal 作为参数。

为了知道中止何时发生,我正在为 signal 实例添加 "abort" 事件的事件监听器。该事件监听器将在每次触发 “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 ,执行多个数据库操作,但我也可以为其提供一个中断信号,以便一次性取消所有操作。

中止错误处理

每次中止事件都会伴随着中止的原因。这使得您可以根据不同的异常原因采取不同的响应措施,从而使应用程序更加灵活。

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

 const controller = new AbortController()

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

// Provide a custom reason to this abort.
controller.abort('user cancellation')

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

结论

如果你在编写 JavaScript 库,其中取消或中止操作有意义,我强烈建议你不要寻找其他 API 了,因为 AbortController API 非常出色!如果你在构建应用程序,当需要取消请求、移除事件监听器、中止流或教给任何逻辑以可取消性时,可以很好地利用 abort controller。

关于本文
译者:@飘飘
作者:@Artem
原文:https://kettanaito.com/blog/dont-sleep-on-abort-controller

这期前端早读课
对你有帮助,帮” 
 “一下,
期待下一期,帮”
 在看” 一下 。