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

news2024/10/9 16:16:04

大家好,我是 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.coloradmin.cn/o/2199457.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

重学SpringBoot3-集成Redis(三)之注解缓存策略设置

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 期待您的点赞&#x1f44d;收藏⭐评论✍ 重学SpringBoot3-集成Redis&#xff08;三&#xff09;之注解缓存策略设置 1. 引入 Redis 依赖2. 配置 RedisCacheManager 及自定义过期策略2.1 示例代码&#xff1a;自定…

重塑能源持续亏损近22亿:今年前五个月销量下滑,产能利用率骤降

《港湾商业观察》黄懿 9月2日&#xff0c;上海重塑能源集团股份有限公司&#xff08;下称“重塑能源”&#xff09;向港交所提交上市申请书&#xff0c;委任中国国际金融香港证券有限公司、招银国际融资有限公司及法国巴黎证券&#xff08;亚洲&#xff09;有限公司为整体协调…

Linux(不同版本系统包含Ubuntu)下安装mongodb详细教程

一、下载MongoDB 在MongoDB官网下载对应的MongoDB版本&#xff0c;可以点击以下链接快速跳转到下载页面&#xff1a; mongodb官网下载地址 注意选择和自己操作系统一致的platform,可以先查看自己的操作系统 查看操作系统详情 命令&#xff1a; uname -a 如图&#xff1a;操…

海洋大地测量基准与水下导航系列之二国外海底大地测量基准和海底观测网络发展现状(下)

2004年&#xff0c;英国、德国、法国等国家在欧洲“全球环境与安全监测’(Global Monitoring for Environment and Security,GMES)观测计划倡导下制定了“欧洲海底观测网络”(European Seafoor Observatory Network,ESONET)计划。ESONET是一个多学科的欧洲卓越网络(NoE &#x…

光路科技以技术创新为驱动,打造创新型企业新标杆

近日&#xff0c;深圳市光路在线科技有限公司&#xff08;光路科技&#xff09;凭借其出色的创新能力和市场表现&#xff0c;荣获深圳市中小企业服务局颁发的“创新型中小企业”称号。这一荣誉标志着光路科技在推动行业发展和技术进步方面取得了显著成就。 光路科技自2008年成立…

【含文档】基于Springboot+Android的在线招聘平台(含源码+数据库+lw)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 系统定…

◇【code】PPO: Proximal Policy Optimization

整理的代码库&#xff1a;https://github.com/Gaoshu-root/Code-related-courses/tree/main/RL2024/PPO OpenAI 文档 —— PPO-Clip OpenAI 文档 界面链接 PPO&#xff1a; on-policy 算法、适用于 离散 或 连续动作空间。可能局部最优 PPO 的动机与 TRPO 一样&#xff1a;…

Scott Brinker:企业正在更换更多的Martech,专注集成和API,不断扩大技术栈

营销技术替代因素&#xff1a;集成和API排在第二位 MarTech.org组织了2024年Martech替代调查&#xff0c;它能够深入了解营销技术栈是如何演变的。在496名受访者中&#xff0c;有65%的人表示他们在过去一年中更换了他们技术栈中的一个或多个营销技术解决方案。这些是最常被替代…

Tableau|三 数据连接与管理

一 Tableau的数据架构 数据连接层&#xff08;Connection&#xff09;、数据模型层&#xff08;DataModel&#xff09;和数据可视化层&#xff08;VizQL&#xff09;。 1.数据连接层 决定了如何访问源数据和获取哪些数据。 数据连接层的数据连接信息包括数据库、数据表、数据视…

华为大咖说 | 新时代,智能电动车车联网有哪些发展趋势?(下篇)

本文作者&#xff1a;朱行健&#xff08;华为专家&#xff09;全文约4252字&#xff0c;阅读约需9分钟 近年来&#xff0c;汽车产业逐步向电动化、自动化、网联化、共享化发展&#xff0c;车联网开始成为新的竞争主体&#xff0c;汽车市场开始出现新的市场发展驱动力、形成新的…

E36.C语言模拟试卷1第一大题选题解析与提示(未完)

点我去下载C语言模拟试卷1的文件 备注:ZIP文件中的参考答案仅仅提供最终结果 目录 第3题 第5题 第7题 第9题 第14题 第16题 第19题 第20题 第22题 第24题 第26题 第27题 第28题 第3题 3.若有说明语句&#xff1a;char c ‘\64’ ; 则变量C包含&#xff1a; …

python19_加减乘除(二)

加减乘除 a hello b world c 2 d 4# 字符串加法 def str_add(A, B):result A Breturn result# 字符串乘法 def str_mul(A, B):result A * Breturn result# 字符串除法 def str_div(A, B):result B / Areturn result# 字符串减法 def str_sub(A, B):result B - Aretur…

A股牛市来袭,资本涌动:加密市场的出路与机遇

近期&#xff0c;随着A股的强劲反弹&#xff0c;不少加密市场的投资者&#xff0c;尤其是一些KOL&#xff08;关键意见领袖&#xff09;&#xff0c;开始转移资金并公开建议进军A股。这种趋势反映出部分投资者对加密市场的信心动摇&#xff0c;尤其是在全球宏观经济不确定性加剧…

python兴农购物网站系统—计算机毕业设计源码38256

摘 要 助农工作是当前我国全面建成小康社会的重点工作&#xff0c;由于我国农村地域广大&#xff0c;贫困人口多&#xff0c;区域差异大&#xff0c;因此&#xff0c;不同区域的扶贫方法也是不一样的。近年来&#xff0c;随着网络的普及。许多农村地区物产丰富&#xff0c;但由…

【解决方案】基于数据库驱动的自定义 TypeHandler 处理器

前言 笔者在最近的项目开发中&#xff0c;频繁地遇到了 Java 类型与 JDBC 类型之间的2个转换问题&#xff1a; 数据库的 varchar 类型字段&#xff0c;需要存储 Java 实体中的 JSON 字符串 数据库的 int 类型字段&#xff0c;需要存储 Java 实体中的 Enum 枚举 其实要处理也不…

数据库软题6.2-关系模式-范式

一、判断部分函数依赖&#xff08;1NF有部分函数依赖&#xff09; 题型&#xff1a;给出函数依赖集和属性&#xff0c;判断该关系模式属于第几范式。 求出候选码 若是候选码为属性的组合&#xff0c;则可能有部分函数依赖&#xff1b;&#xff08;存在部分函数依赖&#xff0…

苹果AI重磅升级!ChatGPT正式登陆iPhone,开启智能生活新时代

苹果AI重磅升级&#xff01;ChatGPT正式登陆iPhone&#xff0c;开启智能生活新时代 在今年的全球开发者大会&#xff08;WWDC 2024&#xff09;上&#xff0c;苹果公司引爆了科技圈——通过与OpenAI合作&#xff0c;苹果宣布将人工智能技术与iPhone深度融合。通过这个合作&…

自由学习记录(2)

Unity打包图集相关 Draw Call 实验设置&#xff1a; 我们将创建两个场景&#xff0c;一个场景有高 Draw Call&#xff0c;另一个场景通过优化减少 Draw Call。然后对比它们的帧率&#xff08;FPS&#xff09;。 场景 1&#xff1a;高 Draw Call 场景&#xff08;无优化&…

浙大数据结构:07-图5 Saving James Bond - Hard Version

这道题也是很有难度&#xff0c;我最开始尝试用Dijkstra来做&#xff0c;发现不是很好处理&#xff0c;用bfs还不错。 机翻&#xff1a; 1、条件准备 n为鳄鱼数量&#xff0c;jump为跳跃距离&#xff0c;headjump为第一次跳跃距离&#xff0c;包括了岛的半径。 isalive标识…

求职书与求职经历 - Chap01.

节前定点在智联投了几家&#xff0c;智联上之前的简历还在&#xff0c;稍稍维护了一下&#xff0c;现在有两三家再看看。然后节后&#xff0c;今天&#xff0c;注册了职友网的7天会员。正在整理简历。 专利证书&#xff0c;通过soopat查&#xff0c;很不友好。国家产权局后来直…