记录---前端中断请求的方式与原理

news/2025/2/28 18:13:24/文章来源:https://www.cnblogs.com/smileZAZ/p/18563334

🧑‍💻 写在开头

点赞 + 收藏 === 学会🤣🤣🤣

Axios.CancelToken

axios对象有一个属性叫CancelToken,该属性提供了中断已经发出去的请求的方式。具体使用方式有两种:

方式一:执行器模式

<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script> const CancelTokenFunc = axios.CancelToken; let cancel;
​ // 发送请求 axios   .get("https://jsonplaceholder.typicode.com/todos/1", {     cancelToken: new CancelTokenFunc(function executor(c) {       // 将 cancel 函数赋值给外部变量       cancel = c;     }),   })   .catch((error) => {      console.log(error.message);   });
​ // 取消请求 setTimeout(() => {   cancel("Operation canceled by the user.");}, 1000);
</script>

在第4行中,我们先获取一个中断构造函数CancelTokenFunc,我们在第10行中用这个构造函数new出一个实例赋值给get请求的参数cancelToken字段。

在调用CancelTokenFunc构造函数new出一个实例的时候,我们传入了一个执行器函数,该执行器会接受一个参数,这个参数就是用来控制中断请求的取消函数,接着我们把该参数函数赋值给外部变量,这样就可以在外部需要的时候执行中断请求的操作。

执行上述代码,将浏览器调整成低速3G模式后,执行结果如下:

 并在控制台中输入了如下信息:

Operation canceled by the user.

方式二:令牌模式

// 创建一个 CancelToken 源
const CancelTokenFunc = axios.CancelToken;
const { token, cancel } = CancelTokenFunc.source();
​
// 发送请求
axios.get("https://jsonplaceholder.typicode.com/todos/1", {   cancelToken: token,}).catch((error) => {   console.log(error.message);});
​
// 取消请求
setTimeout(() => { cancel("Operation canceled by the user.");
}, 1000);

  

在第3行代码中,用CancelTokenFuncsource方法生成一个取消令牌源,并从取消令牌源中解构出tokencancel字段,然后在GET请求中将取消令牌源的token传递给cancelToken,接着在外部调用请求令牌源的cancel方法来取消请求。

执行结果和上面那种方式一样,就不再赘述了。

相比于方式一的执行器模式,方式二的令牌模式更简单易懂,另外需要注意一下,每次调用CancelTokenFunc.source()生成的令牌源是不一样的。

AbortController

AbortController是一个Web API,用于控制和管理可中止的异步操作,例如 fetch 请求、DOM 操作。接下来我们看看怎么用AbortController来中止请求。

<!DOCTYPE html>
<html> <head>   <title>中断请求demo</title> </head> <body>   <script>     // 创建一个 AbortController 信号源     const controller = new AbortController();     const { signal } = controller;
​     // 发送请求     fetch("https://jsonplaceholder.typicode.com/todos/1", {       signal,     }).catch((error) => {       console.log(error);     });
​     // 取消请求     setTimeout(() => {       controller.abort("Operation canceled by the user.");     }, 1000);   </script> </body>
</html>

  

在第9行中,我们创建了一个AbortController信号源,在fetch请求的时候传递一个信号给请求的signal参数,之后便可以在请求的外部通过调用信号源的abort方法来取消请求。

这个API的用法其实和Axios.CancelToken的令牌模式一样,但是该API会有兼容性问题,需要通过引入yet-another-abortcontroller-polyfill或者abortcontroller-polyfill来解决。

 

令牌中断请求原理

中断请求的原理其实很简单,只要监听到调用取消函数,就执行xhr.abort()(其中,xhrXMLHttpRequest的实例)中断请求即可,值得探究的是令牌中断请求的原理,也就是tokencancel之间的映射关系是怎么建立的。

首先我们需要模拟下请求取消的过程,其代码如下:

function fetchData(url, options = {}) { const { cancelToken } = options;
​ return new Promise((resolve, reject) => {   const xhr = new XMLHttpRequest();   xhr.open('GET', url);
​   // 监听请求状态变化,处理请求的常规逻辑   xhr.onreadystatechange = () => {     if (xhr.readyState === 4) {       if (xhr.status >= 200 && xhr.status < 300) {         resolve(xhr.responseText);       }     }   };
​   // 监听取消请求   if (cancelToken) {     // ... 需要在外界调用cancel请求的时候,调用xhr.abort()方法中止请求,     // 并在这里调用reject函数将Promise对象的状态改成rejected   }
​   xhr.send();});
}
fetchData("https://jsonplaceholder.typicode.com/todos/1").then((res) => { console.log(res);
});

  

上述代码中,我们在fetchData中返回一个Promise对象,并在Promise对象新建一个原生的XMLHttpRequest对象。

其中的关键代码,在于监听取消请求这个判断里。

在监听取消请求这个判断中,我们只有一个cancelToken属性,这个属性需要在外界执行cancel时调用xhr.abort()来中止已经发出去的请求,同时将fetchData内的Promise对象的状态改成Rejected

因此,cancelToken需要携带一个回调属性,在外界执行cancel方法时触发回调。

自然而然的,我们就想到,能否给cancelToken挂载一个Promise实例的属性,然后将这个Promise属性的resolved方法传递给cancel,这样,当执行cancel函数的时候,其实就是执行resolve(),从而改变Promise实例的状态,我们就能在Promise实例的then方法中执行需要的操作。

也就是说,监听取消请求需要被设计成这样:

function fetchData(url, options = {}) { const { cancelToken } = options;
​ return new Promise((resolve, reject) => {   const xhr = new XMLHttpRequest();   xhr.open('GET', url);
​   // 监听请求状态变化,处理请求的常规逻辑   // 其他代码
​    // 监听取消请求   if (cancelToken) {     // 需要在外界调用cancel请求的时候,调用xhr.abort()方法中止请求      // 并调用reject函数将Promise对象的状态改成rejected     cancelToken.promise.then((msg) => {       xhr.abort();       reject(new Error(msg));     })   }。   xhr.send();});
}
​

其中,cancelToken.promise是一个Promise实例的属性。

现在,我们继续设计构造函数CancelToken的实现,这个函数需要有一个source方法,该方法返回两个属性,一个是token,一个是cancel函数,其中token应该有一个promise属性,该属性是一个Promise实例,该实例的resolved方法将传递给cancel函数。

 
function CancelToken() {}
CancelToken.source = function () { let cancel; const token = {   promise: new Promise((resolve) => {cancel = resolve})}; return {   cancel,   token,};
};

  

上述代码里,我们将token声明为对象,并在第5行中给token添加一个promise属性,该属性是一个Promise实例,并且将Promise实例的resolve方法传递给了cancel变量,这样,当调用执行cancel()的时候,就是在执行resolve()tokenpromise属性就能触发then回调函数。

这样,我们就实现了令牌中断请求的要求,并将cancel和token关联起来了。到这里,我们就明白每一次调用source方法生成的canceltoken为啥能一一对应了。

执行器模式原理

CancelToken不仅支持令牌中断模式,还支持执行器中断模式,而执行器模式是需要通过CancelToken的构造函数实现。

该构造函数的实现有三个细节需要注意:

  1. 首先,该构造函数同样需要给实例对象挂载一个promise属性,该属性是一个Promise实例。这样才能支持在token.promise.then回调里执行取消操作。
  2. 其次,需要接受一个执行器函数作为入参,
  3. 最后,作为入参的执行器,它本身也有入参,它的入参是一个方法,在这个方法调用的时候,执行promise属性的resolve方法,这样才能触发toekn.promise.then回调。

带着上面三个细节,我们来尝试实现CancelToken构造函数:

function CancelToken(executor) { let resolvePromise; this.promise = new Promise((resolve) => { resolvePromise = resolve;});  executor(function c() {   resolvePromise();})
}

  

上述代码中,我们依照三个细节,来一一解读下:

  1. 对于第一个细节,我们在第3行代码中,我们在this上挂载了promise属性,该属性是一个Promise对象,同时,为了达到在外部触发该Promise对象的状态变更,我们将其resolve方法保存给了外部变量resolvePromise
  2. 对于第二个细节,我们在第1行声明构造函数的时候就声明了executor入参。
  3. 对于第三个细节,我们在第5行中,在执行器调用的时候传入一个函数作为入参,同时在函数内部执行resolvePromise()触发this.promise状态变更。

这样,我们就实现了简单的CancelToken的构造函数。

两个模式结合

接下来我们将执行器模式结合令牌中断模式的代码一起看下:

 
function CancelToken(executor) {let resolvePromise;this.promise = new Promise((resolve) => { resolvePromise = resolve;});executor(function c() {resolvePromise();})
}
CancelToken.source = function () {let cancel;const token = {promise: new Promise((resolve) => {cancel = resolve})};return {cancel,token,};
};

  

结合令牌中断模式和执行器中断模式的代码一起看后,我们发现,第3行中给this.promise赋值了一个Promies实例,第11行中token需要的promise属性,也是一个Promise实例,因此,这两个能优化一下:

function CancelToken(executor) {let resolvePromise;this.promise = new Promise((resolve) => { resolvePromise = resolve;});executor(function c() {resolvePromise();})
}
CancelToken.source = function () {let cancel;const token = new CancelToken(function executor(c) {cancel = c;});return {cancel,token,};
};

  

上述代码中,我们修改了第11行代码,给token赋值为CancelToken实例对象,并在实例化的时候传入一个执行器函数executor,该执行器函数接受一个参数c,并将c赋值给了外部变量cancel属性,这样,执行cancel的流程就变成下面这样:

  1. 调用执行第15行返回的cancel()函数。
  2. cancel函数来自于第11行中executor的入参c
  3. 第11行中的入参c来自于第5行执行executor时的赋值。
  4. 最终,执行cancel()的时候,就会执行第6行中的resolvePromise()方法,从而改变promise属性的状态,触发then回调函数。

测试手写版CancelToken

接下来,使用我们实现的CancelToken来试试取消网络请求,

方式一:执行器模式示例如下:

<script>function CancelToken(executor) {let resolvePromise;this.promise = new Promise((resolve) => {resolvePromise = resolve;});executor(function c() {resolvePromise();});}CancelToken.source = function () {let cancel;const token = new CancelToken(function executor(c) {cancel = c;});return {cancel,token,};};function fetchData(url, options = {}) {const { cancelToken } = options;return new Promise((resolve, reject) => {const xhr = new XMLHttpRequest();xhr.open("GET", url);// 监听请求状态变化,处理请求的常规逻辑xhr.onreadystatechange = () => {if (xhr.readyState === 4) {if (xhr.status >= 200 && xhr.status < 300) {resolve(xhr.responseText);}}};// 监听取消请求if (cancelToken) {cancelToken.promise.then((msg) => {xhr.abort();reject(`Request cancelled: ${msg}`);});}xhr.send();});}let cancel;fetchData("https://jsonplaceholder.typicode.com/todos/1", {cancelToken: new CancelToken((c) => {cancel = c;}),}).catch((e) => {console.log(e);});setTimeout(() => {cancel("取消请求");}, 500);
</script>

将网速调整成慢速3G后执行后效果如下:

 控制台打印的结果,有个undefined

 方式二:令牌模式示例如下:

<script>function CancelToken(executor) {// ...}CancelToken.source = function () {// ...};function fetchData(url, options = {}) {// ...}const { token, cancel } = CancelToken.source();fetchData("https://jsonplaceholder.typicode.com/todos/1", {cancelToken: token,}).catch((e) => {console.log(e);});setTimeout(() => {cancel("取消请求");}, 500);
</script>

执行结果同执行器模式,这里就不截图了。

优化

我们手写版的CancelToken已经实现了基本的功能,也就是取消请求,但是有个问题,那就是调用cancel("取消请求")里,参数没有传递到给cancelToken.promise.then回调函数,所以打印出来的结果里有个undefined。因此,我们需要稍微优化下CancelToken,补齐参数的传递。

优化的方式也很简单,取消函数cancel的入参,会通过形参赋值的方式传递给c的入参,因此我们只需要拿c的入参给resolve就行了。具体如下:

function CancelToken(executor) {let resolvePromise;this.promise = new Promise((resolve) => {resolvePromise = resolve;});executor(function c(msg) {resolvePromise(msg); // 这里将cancel的入参传递给resolve});
}

这样,就完成了参数的传递。

还有一点需要注意,那就是cancel可能会被多次调用,我们需要在第二次之后的调用直接结束。这里我们就可以在第一次调用cancel的时候用传入的参数做个标记,有参数则代表已经调用过cancel,后续再调用cancel时直接返回,这样就能防止多次调用。

function CancelToken(executor) {let resolvePromise;this.promise = new Promise((resolve) => {resolvePromise = resolve;});const token = this;executor(function c(msg) {if (token.reason) {return; // 如果已经有了reason,说明之前调用过cancel,后续再次调用直接接结束}token.reason = msg || 'cancel request';resolvePromise(token.reason); // 这里将cancel的入参传递给resolve});
}

上述代码中,我们在executor的外部,也就是第7行先保存this指向为token,然后在第9行中判断是token是否存在取消原因字段reason,有的话,说明之前已经调用过cancel了,这时再次调用cancel就是重复执行cancel方法,我们可以直接retuen从而避免重复取消请求。

在第12行中,我们给token.reason赋了一个默认值cancel request,因为第一次调用cancel时有可能没传参。

这样,我们就完成了CancelToken的手写版优化,完整代码如下:

function CancelToken(executor) {let resolvePromise;this.promise = new Promise((resolve) => {resolvePromise = resolve;});const token = this;executor(function c(msg) {if (token.reason) {return;}token.reason = msg || 'cancel request';resolvePromise(token.reason);});
}
CancelToken.source = function () {let cancel;const token = new CancelToken(function executor(c) {cancel = c;});return {cancel,token,};
};

  

本文转载于:https://juejin.cn/post/7395446371031728169

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/839349.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

swiper最外层设置了 overflow hidden ,但是子元素有动画或者弹窗需要超出,

swiper 外层 .swiper-container、swiper-wrapper、swiper-slide 都存在 position:relative 样式, 直接 子元素absolute 无效 干脆去掉 overflow:hidden,但这样 swiper 其他本该隐藏切换显示的 swiper-slide 也显示出来了,页面混乱 无效 方案: 放大 swiper-container 的…

鸿蒙NEXT开发案例:数字统计

【引言】 本文将通过一个具体的案例——“数字统计”组件,来探讨如何在鸿蒙NEXT框架下实现这一功能。此组件不仅能够统计用户输入文本中的汉字、中文标点、数字、以及英文字符的数量,还具有良好的用户界面设计,使用户能够直观地了解输入文本的各种统计数据。 【环境准备】 •…

浙江省科技进步奖一等奖!阿里云云原生技术实现新突破

11 月 22 日, 2023 年度浙江省科学技术奖获奖成果公布,阿里云与浙江大学、支付宝、谐云科技联合完成的基于云原生的大规模云边协同关键技术及应用获得浙江省科学技术进步一等奖。科技成果鉴定委员会高度评价该技术,“项目研发难度大,成果创新性强,对促进关键技术进步及自主…

jenkins导入和导出视图的所有job

目标:导出http://192.168.31.32:8080/上Jenkins的内蒙古智慧园区test-后端view视图下的所有job,然后导入到http://192.168.20.143:8080/上Jenkins的内蒙古智慧园区test-后端view视图,并且每个job名字在原来的基础上加test-前缀 一、导出 1、在导出jobs配置的jenkins上配置认…

MAE

一、大体内容 PDF: https://arxiv.org/abs/2111.06377 CODE: https://github.com/facebookresearch/mae (原文采用tensorflow 和 TPU 没有开源) 前面已经介绍了Transformer、BERT、ViT,这里理一下大概关系。首先Transformer将注意力机制引入到NLP领域,得到很大的提升,接着BE…

数字孪生如何赋能智慧工厂?核心作用详解

近年来,随着智能制造的发展,数字孪生技术逐渐成为智慧工厂的核心技术之一。它通过将物理世界的生产设备、流程和环境实时映射到虚拟空间,实现了物理与数字的无缝连接。这种融合不仅提升了工厂运营效率,还为企业实现更高层次的智能化转型奠定了基础。那么,数字孪生在智慧工…

python+pymysql(16)

python操作mysql 一、python操作数据库 1、下载pymysql 库, 方法一:pip3 install pymysql 或pip install pymysql方法二:在pycharm中setting下载pymysql =============================== 2、打开虚拟机上的数据库=============================== 3、pymysql连接(1…

量化存储墙以及功耗优化空间

CMOS 计算和存储工艺发展步调并不一致,SOTA Memory 最高频率的远远低于 SOTA CMOS。如下图所示[1]:除了性能,能耗上存储也远远高于计算能耗,存储能耗受诸多譬如大小、宽度等参数影响,但不妨粗略进行数量级估计计算,如图 2014 年 ISSCC 经典的数据 45 nm 0.9V 下,计算和能…

国产云游戏平台价格盘点,性价比首选竟是ToDesk!

当5G逐渐走进人们的生活,游戏行业随之迎来云游戏的热潮。无需高端硬件设备,只需稳定的网络连接,就能畅玩各种游戏大作,云游戏已成为用户玩游戏的首选平台。 短短两年时间,国内涌现出大大小小数十家云游戏平台,各平台基本围绕“低价低配玩3A大作”、“手机畅玩各类端游”进…

SOLIDWORKS软件是免费使用的吗?

SOLIDWORKS是一款三维计算机辅助设计软件,它被广泛应用于建筑设计、机械设计、产品设计、工业设计等领域。SOLIDWORKS提供了强大的建模工具,可以用于创建各种复杂的三维模型,包括机械结构、零件、装配等。此外,它还拥有直观的用户界面和操作方式,支持多种文件格式,方便用…

远程连接 USB 设备

需要的工具: ① USB redirector technician edition(服务端)(我们自己的电脑) ② USB Redirector Technician Edition-custormer module(客户端)(客户的电脑) 设置 USB Redirector Technician Edition客户端的操作。 1、在客户端直接双击打开 USB Redirector Technician Edi…

vue2-vuex

专门在 Vue 中实现集中式状态(数据)管理的一个 Vue 插件,对 vue 应 用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信 应用场景:多个组件依赖于同一状态、共享数据 来自不同组件的行为需要变更同一状态 vuexvuex原理每一…