写点什么

由一个 bug 引发对 axios 的刨根问底

  • 2019-09-27
  • 本文字数:4658 字

    阅读完需:约 15 分钟

由一个bug引发对axios的刨根问底

在做一个 vue 技术栈的 h5 项目的时候,出现了这么一个 bug,不同路由下的请求接口是同一个,如果在网络较慢的情况下进行路由的快速切换就会导致两个路由下的数据混在一起,以下是从解决这个 bug 引发的一系列思考。

1 问题现象

网络调为 mid-tier mobile 后,快速切换路由会发现第二个路由(待客户签署)的数据和第一个路由(起草协议)数据发生了累加:



问题现象

2 解决效果

添加了路由切换后取消请求的功能后数据正常:



解决效果

3 具体实现方式

初始入口文件中通过 axios 生成 cancelToken:



axios 的拦截器的 request 配置中添加参数 cancelToken


4 原理分析

1.axios 简介

首先我们根据 axios 官方文档可以知道,axios 原生支持取消请求:



axios 文档里介绍的取消 axios 请求有以下两种方式:


// 第一种:使用 CancelTokenconst { CancelToken, isCanCel } = axios;const source = CancelToken.source();
axios.get('/user/12345', { cancelToken: source.token}).catch(thrown => { if (isCancel(thrown)) { // 获取 取消请求 的相关信息 console.log('Request canceled', thrown.message); } else { // 处理其他异常 }});
axios.post('/user/12345', { name: 'new name'}, { cancelToken: source.token})
// 取消请求。source.cancel('Operation canceled by the user.');
// 第二种:还可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token:const CancelToken = axios.CancelToken;let cancel;
axios.get('/user/12345', { cancelToken: new CancelToken(function executor(c) { // executor 函数接收一个 cancel 函数作为参数 cancel = c; })});
// 取消请求cancel();
复制代码


那么它究竟是怎么做到的?


第一步我们先要清楚一个请求在 axios 的工作流程,像一个管道一样:



axios 请求流转


看上图发现 request 发出之前有个 interceptors,查看 axios 文档不难发现,这个 interceptors 提供了设置请求或响应被处理之前拦截到请求或响应去处理他们(其实就是个 promise 中间件):


2.源码探究

其实我们在开始的问题解决代码的贴图中就是使用了 interceptors 对 request 发出前添加了 cancelToken 配置,那么问题来了:


<1>.为什么在这里加了 cancelToken 的配置就可以实现取消未完成的请求呢?


<2>. 是 axios.interceptors.request 的魔力吗,怎么会如此神奇呢?


带着问题我们一步一步看:


<1>、各类请求的公共方法 request


在源码文件 core/Axios.js 有这么几行核心代码,无论请求方法是什么类型的都会走一个 request 方法:



那么这个 request 到底做了什么?



粗略的解释上图代码:


1、定义了一个数组 chain,这个数组中包含了 dispatchRequest 对象和 undefined 两个元素。


2、将请求传入的 config 对象转为 promise 对象。


3、经过拦截器处理后的 chain 变为:


[dispatchRequest, undefined,


interceptor.response.fulfilled,


interceptor.response.rejected]


4、返回一个 promise 链式调用之后的的执行之后的返回结果:


Promise.resolve(config)


.then(interceptor.request.fulfilled, interceptor.request.rejected)


.then(dispatchRequest, undefined)


.then(interceptor.response.fulfilled, interceptor.response.rejected)


下面用一幅图直观的描述 axios 的 promise 链式调用:



promise 链式调用


看到这里想必大家都会有疑问,拦截器相关的 interceptor.request.fullfilled 和 interceptor.request.rejected 等是什么,怎么来的,有什么用?请看下一个 Interceptor 源码解析。


<2>、Interceptor 源码解析


interceptor.request.fullfilled 和 interceptor.request.rejected 是什么?


答:是两个函数,分别是 promise 在 resolve 和 reject 状态的回调函数。


interceptor.request.fullfilled 和 interceptor.request.rejected 怎么来的,有什么用?


源码里提供了 InterceptorManeger.js 文件,用来创建 interceptor:



创建 interceptor.request.fullfilled 和 interceptor.request.rejected 过程:



此图的详细解释如下:


/*Axios.js核心代码*/Axios.prototype.request = function request(config) {  ...// 在这里调用了forEach方法,而这个方法在下边InterceptorManager.js中  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {    // 将参数interceptor加到chain数组头部    chain.unshift(interceptor.fulfilled, interceptor.rejected);  });
while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); }
return promise;};
/*InterceptorManager.js文件核心代码*/function InterceptorManager() { this.handlers = []; // 用来保存所有拦截器注册的函数}
// 这个use方法用来给拦截器注册回调函数InterceptorManager.prototype.use = function use(fulfilled, rejected) { // 这里的handles中存的是由fulfilled和rejected组成的object this.handlers.push({ fulfilled: fulfilled, rejected: rejected }); return this.handlers.length - 1;};
...
/* 将所有handles中注册的函数{ fulfilled: fulfilled, rejected: rejected }, 遍历给fn(这里的fn可以理解为unshiftRequestInterceptors)执行一遍, 即作为unshiftRequestInterceptors函数的参数*/InterceptorManager.prototype.forEach = function forEach(fn) { utils.forEach(this.handlers, function forEachHandler(h) { if (h !== null) { fn(h); } });};
/*utils.js核心代码*/function forEach(obj, fn) { ... if (isArray(obj)) {// fn(理解为unshiftRequestInterceptors)遍历执行,参数是this.handles中的元素 for (var i = 0, l = obj.length; i < l; i++) { fn.call(null, obj[i], i, obj); } } ...}
复制代码


讲到这里还是没有看到 cancelToken,接着往下看。


<3>、dispatchRequest 源码解析


interceptor.request 逻辑执行结束之后,就到 dispatchRequest 了,查看源码我们发现这才是真正发出请求的地方,截取的浏览器端发请求的核心代码如下:


/*dispatchRequest.js核心代码*/module.exports = function dispatchRequest(config) {  ...  // 这里的adapter其实就是获取发送请求的方式,浏览器中为xhr,node端为http  var adapter = config.adapter || defaults.adapter;
/* 这里then参数中为promise的两个回调函数,那么adapter这个promise是什么样的, 请看下边的xhr.js源码分析 */ return adapter(config).then(function onAdapterResolution(response) { throwIfCancellationRequested(config);
// Transform response data response.data = transformData( response.data, response.headers, config.transformResponse );
return response; }, function onAdapterRejection(reason) { if (!isCancel(reason)) { throwIfCancellationRequested(config);
// Transform response data if (reason && reason.response) { reason.response.data = transformData( reason.response.data, reason.response.headers, config.transformResponse ); } }
return Promise.reject(reason); });};
复制代码


其实这里不难发现第一次画的 promise 链式调用图中,在 dispatchRequest 函数内部又有 promise 的链式调用:



这时候的 promise 链式调用就变成了这个样子:



promise 链式调用 2


dispatchRequest 分解后的形成了又一个以 adapterPromise 开始的 promise 链式调用,而这个 adapter 在浏览器中其实就是 Xhr.js 返回的 promise 对象,这里也是真正发出请求的地方。


通过查看源码我们终于发现!!!其中有对 cancelToken 的处理逻辑!!!


那么 cancelToken 到底是怎么起作用的? 请看下一节,xhr.js 源码分析:


<4>、xhr.js 源码解析


module.exports = function xhrAdapter(config) {  return new Promise(function dispatchXhrRequest(resolve, reject) {...    if (config.cancelToken) {      /*       如果config中有cancelToken参数,则讲onCanceled注册为             config.cancelToken.promise的resolve回调函数,在onCanceled方法中就可以取消请求,并将adapter.promise状态变为reject      */      config.cancelToken.promise.then(function onCanceled(cancel) {        if (!request) {          return;        }
request.abort(); reject(cancel); // Clean up request request = null; }); }... });};
复制代码


其实看到这里大家应该已经看懂了,在这个 cancelTokenPromise 状态变为 rejected 的话整个 promise 链都变为 rejected,请求也就会被取消掉。


那么问题又来了,cancelToken.promise 是什么,这个 promise 什么时候会执行 resolve 方法进行请求取消呢?请看下面的源码分析。


<5>、CancelToken 源码解析


function CancelToken(executor) {...  var resolvePromise;  // 在这里将cancelToken.promise的resolve函数赋值给了resolvePromise变量  this.promise = new Promise(function promiseExecutor(resolve) {    resolvePromise = resolve;  });
var token = this; // 在执行executor这个方法时候会执行resolvePromise,即xhr.js中注册的onCancel,那什么时候会执行executor呢?请看接下里的source方法 executor(function cancel(message) { if (token.reason) { // Cancellation has already been requested return; }
token.reason = new Cancel(message); resolvePromise(token.reason); });}.../*看看源码得知实际调用axios.cancelToken.source()方法生成取消令牌的时候实际上生成了包含两个属性(token,cancel)的对象,在执行cancel方法的时候就会执行上述方法executor,也就是说这里的cancel一执行就执行了onCancel方法,就会取消请求*/CancelToken.source = function source() { var cancel; var token = new CancelToken(function executor(c) { cancel = c; }); return { token: token, cancel: cancel };};
复制代码



在执行了 cancel 之后就会 cancelToken.promise 就变为 reject 了,~~整个 promise 调用就都是 rejected 了,接着 adapter 这个 promise 对象也变为 reject 了,整个 promise 链都为 reject 了,请求取消,game over🙃🙃🙃。


用一幅图表示如下:



<6>、回头望月


再回过头看看我们最开始的解决方案,有没有恍然大悟?


其实我们的解决方式就是同一个路由下的请求公用一个 canceltoken,虽然多个请求会生成多个 promise 链,但是在 adapterPromise 局部的 cancelToken.promise 却是同一个,这样在执行 axios.cancelToken.source().cancel 方法时候就会作用于全部 promise 链,一旦 cancel 一执行,所有未完成的请求都会取消,相对应的 promise 链都会变为 rejected。


初始入口文件中通过 axios 生成 cancelToken:



axios 的拦截器的 request 配置中添加参数 cancelToken


5 思考:知道原理后我们还能做什么?

可以通过 axios 取消特定的未完成请求。


学习 axios 对 promise 的妙用,比如参考 axios 请求取消的实现方式我们是不是也可以对 fetch 请求进行包装呢?


作者介绍:


孟浩然(企业代号名),目前负责贝壳人店平台中心相关前端工作。


本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。


原文链接:


https://mp.weixin.qq.com/s/2ADjfJPge391xpdikM08qQ


2019-09-27 11:172181

评论

发布
暂无评论
发现更多内容

WAIC 2024盛大召开,天翼云以全栈智算能力赋能AI时代!

天翼云开发者社区

人工智能大会 人工智能生态论坛

什么是跨链交换,以bitget钱包为例

大瞿科技

安全可信| 通过双项测试!TeleDB实力亮剑!

天翼云开发者社区

数据库 云计算 天翼云

PDF专业制作与编辑软件Acrobat Pro DC 2021 for Mac v21.中文激活版

Mac相关知识分享

PDF 办公

Redis Desktop Manager for Mac(Redis可视化工具)v2020中文激活版

Mac相关知识分享

可视化工具 数据库设计软件

上榜!天翼分布式云操作系统入选“科创中国”先导技术榜单!

天翼云开发者社区

云计算 分布式 云服务

软件测试学习笔记丨接口请求头

测试人

软件测试 接口测试 测试开发

博睿数据受邀出席深圳地区数智金融交流会 分享《云原生时代的高级可观测性》

博睿数据

“文心•跨模态大模型”又有新动态,支持内容分析时输出自定义标签库

爱AI的猫猫头

音视频 低代码 大模型

【JavaScript】聊一聊js中的浅拷贝与深拷贝与手写实现

快乐非自愿限量之名

JavaScript 拷贝

Microsoft Remote Desktop for Mac(远程桌面连接工具) 10.9.5正式版

Mac相关知识分享

Mac远程控制软件

CAD绘图软件Autodesk AutoCAD 2021 for Mac v2021.1.2中文激活版

Mac相关知识分享

SecureCRT Mac版:强大的SSH与远程连接工具

Mac相关知识分享

深度解读昇腾CANN模型下沉技术,提升模型调度性能

华为云开发者联盟

人工智能 华为云 大模型 华为云开发者联盟 企业号2024年7月PK榜

预告 | 博睿数据将亮相第四届中国新能源汽车产业数智峰会

博睿数据

《好战略,坏战略2》摘录

充实的orzi

企业管理 战略

Parallels Desktop 19一键激活版:强大的Mac虚拟机解决方案

Mac相关知识分享

MomoAl (MetaOasis)是什么?,以bitget 钱包为例

加密眼界

int(1) 和 int(10) 有什么区别?

源字节1号

小程序 开源 `后端 前端‘’

WAIC 2024,好city啊!

天翼云开发者社区

人工智能 世界人工智能大会 WAIC

如何保护你的网络安全?

网络安全服务

TCP 服务器 DDoS waf DDoS 攻击

实战案例:用百度千帆大模型API开发智能五子棋

幂简集成

百度 AI API API 开发

Web3 游戏周报(7.07-7-13)

Footprint Analytics

链游

Go语言设计模式:使用Option模式简化类的初始化

伤感汤姆布利柏

今天我们来聊Java IO模型,BIO、NIO、AIO三种常见IO模型

EquatorCoco

Java IO模型

数据库管理软件Navicat Premium 15 for Mac

Mac相关知识分享

分页查询及其拓展应用案例

快乐非自愿限量之名

分页查询

RaftKeeper v2.1.0版本发布,性能大幅提升!

京东科技开发者

精准搜索:本地文件检索工具的高效策略

京东科技开发者

企业跨省组网:SD-WAN解决方案

Ogcloud

SD-WAN 企业组网 SD-WAN组网 SD-WAN服务商 SDWAN

由一个bug引发对axios的刨根问底_文化 & 方法_孟浩然_InfoQ精选文章