写点什么

由一个 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:172154

评论

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

Dapp开发技术指南,快速入门,掌握实战技能

区块链软件开发推广运营

dapp开发 区块链开发 链游开发 NFT开发 公链开发

几个有趣的C/C++语言『冷知识』

伤感汤姆布利柏

C语言 编程语言、 c++语言

如何避免被公司裁掉?

小齐写代码

熬了一晚上,开放签电子签章移动端上线了~

开放签开源电子签章

电子合同 电子签章

Character Animator 2024 (Ch2024角色动画设计软件) Mac/win

Rose

亲测可用!Acrobat Pro DC 2023中文破解版 Mac/win

Rose

直播回顾 | 张乐、何勉、余伟、任晶磊共议研发效能的破局之道

思码逸研发效能

BRC20铭文智能合约系统开发规则指南

区块链软件开发推广运营

dapp开发 区块链开发 链游开发 NFT开发 公链开发

NFTScan | 02.19~02.25 NFT 市场热点汇总

NFT Research

NFT NFT\ NFTScan

Excelize 开源基础发布 2.8.1 版本,2024 年首个更新

xuri

开源 Excel 开发工具 go语言 Excelize

喜报 | 思码逸 DevInsight 通过DaoCloud兼容性互认证

思码逸研发效能

2023 re:Invent 用 Amazon Q 打造你的知识库

亚马逊云科技 (Amazon Web Services)

人工智能

青团社:亿级灵活用工平台的云原生架构实践

阿里巴巴云原生

阿里云 云原生 可观测

是什么增加了系统的复杂度

Bruce Talk

敏捷开发 Agile Product Owner

Solidity案例详解(三)飞机管理合约

BSN研习社

区块链 Solidity

运用 Argo Workflows 协调 CI/CD 流水线

SEAL安全

开源 Kubernetes CI/CD

FxFactory 8 Pro视觉效果插件:从粒子系统到3D动画

Rose

免费好用的菜单栏Cpu可视化监测工具:跑猫RunCat for mac

Rose

Pydantic:强大的Python 数据验证库

霍格沃兹测试开发学社

AI 编程如何颠覆生产力 | 参与体验免费领取 ArchSummit 架构师峰会专属门票

阿里巴巴云原生

阿里云 云原生

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