当前位置: 首页 > news >正文

这个 JavaScript API 比你想象中更强大!

大家好,我是 ConardLi。

今天,我们来聊聊一个可能被你忽视,而且非常强大的标准 JavaScript API - AbortController

在过去,大家在提到 AbortController 的时候,一般会举请求中断的例子,就连 MDN 给到的描述也是这样的:

但是 AbortController 的能力可不止于此,AbortController 是 JavaScript 中的一个全局类,它可以用来终止任何异步操作,使用方法如下:

const controller = new AbortController();controller.signal;
controller.abort();

我们创建一个 AbortController 实例后,会得到两个东西:

  • signal 属性,这是一个 AbortSignal 实例,我们可以将它传递给要中断的 API,来响应中断事件并进行相应处理,例如,传递给 fetch() 方法就可以终止这个请求了;
  • .abort() 方法,调用这个方法会触发 signal 上的中止事件,并将信号标记为已中止。

我们可以通过监听 abort 事件,然后根据特定的逻辑实现中止:

controller.signal.addEventListener('abort', () => {// 实现中止逻辑
});

洗面我们来了解一些支持 AbortSignal 的标准 JavaScript API。

用法

事件监听器

我们可以在添加事件监听器时提供一个中止 signal,这样在中止发生时,监听器会自动删除。

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

如果我们调用 controller.abort() ,就会从 window 中删除 resize 监听器。这是一种非常优雅的处理事件监听器的方式,因为我们不再需要抽象监听器函数来调用 removeEventListener()

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

如果应用的不同部分负责删除监听器,传递一个 AbortController 实例会更加方便,然后我就发现可以使用单个 signal 来删除多个事件监听器!

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.abort();};
}, []);

在上面的例子中,我在 React 中添加了一个 useEffect() 钩子,其中引入了一些具有不同用途和逻辑的事件监听器。然后,在清理函数中,我只需调用一次 controller.abort() 就可以删除所有添加的监听器,还是很好用的!

Fetch 请求

fetch() 函数也支持 AbortSignal,中断请求应该也是 AbortController 使用最多的场景。

一旦 signal 上的 abort 事件被触发,fetch() 函数返回的请求 Promise 就会被拒绝,从而终止未完成的请求。

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>文件上传示例</title>
</head>
<body><input type="file" id="fileInput" /><button id="uploadButton">上传</button><button id="cancelButton">取消上传</button><script>function uploadFile(file) {const controller = new AbortController();// 将中止信号传递给 fetch 请求const response = fetch('/upload', {method: 'POST',body: file,signal: controller.signal,});return { response, controller };}document.getElementById('uploadButton').addEventListener('click', () => {const fileInput = document.getElementById('fileInput');const file = fileInput.files[0];if (!file) {alert('请选择一个文件');return;}const { response, controller } = uploadFile(file);response.then(res => res.json()).then(data => {console.log('文件上传成功:', data);}).catch(err => {if (err.name === 'AbortError') {console.log('文件上传被取消');} else {console.error('文件上传失败:', err);}});// 保存 controller 以便取消上传window.currentUploadController = controller;});document.getElementById('cancelButton').addEventListener('click', () => {if (window.currentUploadController) {window.currentUploadController.abort();console.log('点击了取消上传按钮');} else {console.log('没有正在进行的上传操作');}});</script>
</body>
</html>

上面例子中的 uploadFile() 函数发起了一个 POST 请求,返回关联的 response Promise 以及一个 AbortController 实例,当用户点击取消上传按钮的时候,我们可以通过 AbortController 实例随时终止这个请求。

Node.js 中由 http 模块发出的请求也支持 signal 属性!

const http = require('http');
const { AbortController } = require('abort-controller');function makeRequest() {const controller = new AbortController();const options = {hostname: 'example.com',port: 80,path: '/',method: 'GET',// 将 AbortSignal 传递给请求signal: controller.signal};const req = http.request(options, (res) => {let data = '';res.on('data', (chunk) => {data += chunk;});res.on('end', () => {console.log('Response:', data);});});req.on('error', (e) => {if (e.name === 'AbortError') {console.log('请求被取消');} else {console.error(`请求遇到问题: ${e.message}`);}});req.end();// 模拟取消操作,例如在 2 秒后取消请求setTimeout(() => {controller.abort();}, 2000);
}makeRequest();

AbortSignal 类的静态方法

AbortSignal 类还具有一些静态方法,可以简化 JavaScript 中的请求处理。

AbortSignal.timeout

我们可以使用 AbortSignal.timeout() 静态方法作为快捷方式,创建一个在经过一定超时时间后会触发中止事件的信号。如果只想在请求超时后取消请求,就不需要创建一个 AbortController 了:

    document.getElementById('fetchButton').addEventListener('click', () => {const url = 'https://jsonplaceholder.typicode.com/posts/1'; // 示例 API 地址fetch(url, {// 如果请求超过 1700 毫秒则自动中止signal: AbortSignal.timeout(1700),}).then(response => {if (!response.ok) {throw new Error('网络响应失败');}return response.json();}).then(data => {console.log('请求成功:', data);}).catch(error => {if (error.name === 'AbortError') {console.error('请求超时被取消:', error);} else {console.error('请求出错:', error);}});});

AbortSignal.any

类似于 Promise.race() 处理多个 Promise 的方式,我们可以使用 AbortSignal.any() 静态方法将多个中止信号组合到一个里面,下面是一个具体的例子:

<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>AbortSignal.any 示例</title>
</head>
<body><button id="stopButton">停止监听</button><script>const publicController = new AbortController();const internalController = new AbortController();// 创建 WebSocket 连接const socket = new WebSocket('wss://conardli.websocket.org');// 当 WebSocket 连接打开时触发socket.addEventListener('open', () => {console.log('WebSocket 连接已建立');socket.send('Hello WebSocket!');});// 处理收到的消息function handleMessage(event) {console.log('收到消息:', event.data);}// 使用 AbortSignal.any 将多个中止信号组合到一个中socket.addEventListener('message', handleMessage, {signal: AbortSignal.any([publicController.signal, internalController.signal]),});// 模拟取消操作document.getElementById('stopButton').addEventListener('click', () => {publicController.abort();console.log('停止监听消息事件');});// 也可以通过内部控制器取消setTimeout(() => {internalController.abort();console.log('内部控制器自动中止监听');}, 5000);</script>
</body>
</html>
  1. 创建了两个 AbortController 实例,分别为 publicControllerinternalController
  2. 使用 WebSocket 建立连接,并在连接建立后发送一条消息。
  3. 为 WebSocket 的 message 事件添加监听器,并通过 AbortSignal.any 组合两个中止信号。
  4. 在页面中添加了一个按钮,当点击按钮时将调用 publicController.abort() 来停止监听消息事件。
  5. 另外,使用 setTimeout 模拟了一个内部控制器在 5 秒后自动中止监听操作。

通过这种方式,可以灵活地组合多个中止信号,当任意一个信号触发时,相关的事件监听器都会被取消。

取消流

我们还可以使用 AbortControllerAbortSignal 来取消流。

在下面个例子中,我们创建一个 WritableStream,并通过监听 controller.signalabort 事件来处理流的中止操作。

<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>取消流操作示例</title>
</head>
<body><button id="cancelButton">取消写操作</button><script>async function example() {const abortController = new AbortController();const stream = new WritableStream({write(chunk, controller) {console.log('正在写入:', chunk);// 监听中止信号controller.signal.addEventListener('abort', () => {console.log('写操作被取消');// 处理流中止逻辑,例如清理资源或通知用户});},close() {console.log('写入完成');},abort(reason) {console.warn('写入中止:', reason);}});const writer = stream.getWriter();// 模拟写入操作writer.write('数据块 1');writer.write('数据块 2');// 保存 abortController 以便取消操作window.currentAbortController = abortController;writer.releaseLock(); // 创建新写操作前释放写锁// 监听取消按钮的点击事件document.getElementById('cancelButton').addEventListener('click', async () => {if (window.currentAbortController) {await writer.abort();window.currentAbortController.abort();console.log('点击了取消写操作按钮');} else {console.log('没有正在进行的写操作');}});}example();</script>
</body>
</html>

WritableStream 控制器暴露了 signal 属性,即同样的 AbortSignal。这样,我可以调用 writer.abort(),这会在流的 write() 方法中的 controller.signal 上冒泡触发中止事件。

让任何逻辑变得可中止

实际上 AbortController 的能力还不仅于此,我们可以借助它让任何逻辑变得可中止!

比如下面的例子,我们将 AbortController 添加到 Drizzle ORM 事务中,让我们能够一次取消多个事务。

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 错误来回滚整个事务(这等同于调用 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 实例来创建一个自定义可中止事务。从现在开始,我可以像通常在 Drizzle 中一样使用这个自定义事务来执行多个数据库操作,还可以为其提供一个中止 signal ,以一次取消所有操作。

中止错误处理

每个中止事件都附带中止原因,这让我们能够进行更多的定制化,我们可以对不同的中止原因做出不同的反应。

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

    async function fetchData() {const controller = new AbortController();const signal = controller.signal;// 监听 abort 事件,并打印中止原因signal.addEventListener('abort', () => {console.log('请求中止原因:', signal.reason); // 打印自定义的中止原因});try {const response = await fetch('https://jsonplaceholder.typicode.com/posts/1', { signal });const data = await response.json();console.log('请求成功:', data);} catch (error) {if (error.name === 'AbortError') {console.error('请求因中止被取消:', error.message);} else {console.error('请求出错:', error.message);}}// 保存 controller 以便取消操作window.currentAbortController = controller;}fetchData();// 监听取消按钮的点击事件document.getElementById('cancelButton').addEventListener('click', () => {if (window.currentAbortController) {window.currentAbortController.abort('用户取消了请求');  // 提供自定义的中止原因console.log('点击了取消请求按钮');} else {console.log('没有正在进行的请求');}});

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

兼容性

AbortController 的兼容性非常好,很久之前就已经被纳入了 Web 兼容性 Baseline,从 2019 年 3 月起,就可在所有主流浏览器中使用了。

最后

抖音前端架构团队目前放出不少新的 HC ,有看起会的小伙伴可以看看这篇文章:抖音前端架构团队正在寻找人才! FE/Client/Server/QA,25 届校招同学可以直接用内推码:DRZUM5Z,或者加我微信联系。

如果你想加入高质量前端交流群,或者你有任何其他事情想和我交流也可以添加我的个人微信 ConardLi 。


http://www.mrgr.cn/news/46143.html

相关文章:

  • 国产长芯微LPA1311高性能隔离放大器P2P替代AMC1311替代ACPL-C87A-000E
  • 74.【C语言】文件操作(1)
  • Unity3D Compute Shader同步详解
  • 一文彻底搞懂大模型 - Hugging Face Transformers
  • Fetch获取内容乱码问题
  • 中医文献学:概念、历史、现状与展望
  • 力扣10.9
  • 2024年新能源汽车Q3季报:传统车企车机表现逆袭,问界M7暴露最大短板
  • sql-lab打不开网页了(没解决)
  • linux安装Go ImageMagick插件
  • 基于Kafka2.1解读Producer原理
  • 【Pydantic】如何在 Pydantic 中实现严格模式校验
  • Gpt4.0最新保姆级教程开通升级
  • 滚雪球学Redis[2.0讲]:Redis的数据类型与基本操作
  • L1和L2正则化的区别
  • 校园二手交易平台的小程序+ssm论文源码调试讲解
  • 并发编程-任务创建、执行-Demo
  • 项目管理之精准管理规划:引领团队迈向成功之路
  • 手把手教你如何配置好VS Code的WEB基础开发环境(保姆级)
  • 研发中台拆分之路:深度剖析、心得总结与经验分享