QCon 演讲火热征集中,快来分享技术实践与洞见! 了解详情
写点什么

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

评论

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

会计CRM系统软件提高公司管理效率

低代码小观

企业 企业管理 管理会计综合实训平台 CRM 管理系统

2021大厂安卓知识点总结,字节跳动学习笔记

android 程序员 移动开发

金九银十,面试必备!耗时一周整理的牛客网上最火Java面试八股文

Java 程序员 架构 面试 大厂

QCon看点|亚马逊云科技可持续软件工程实践分享

亚马逊云科技 (Amazon Web Services)

软件工程 S3 云端

老凡尔赛了!当亚马逊云科技大佬“转行”讲起脱口秀

亚马逊云科技 (Amazon Web Services)

数字化转型 设计师

阿里云混合云Apsara Stack 2.0发布,加速政企数智创新

架构 操作系统 公有云 科技

阿里云隐私增强计算产品DataTrust助力产业间实现数据价值高效协同

Lily

Android架构之网络优化

轻口味

android 10月月更

docker 安装kafka

大可大大大

数实融合·绽放新机,Techo Day技术回响日邀您“云相聚”

腾讯云数据库

数据库 tdsql

阿里云正式开源PolarDB-X数据库,壮大云原生分布式数据库生态

Lily

DeFi去中心化DAPP系统软件开发案例(现成)

DeFi平台挖矿系统需求开发(现成案例)

架构设计第一周学习总结

周文

总结思考

Week 1命题作业

小朱

架构实战营

腾讯云,五轮面试,六个小时,灵魂拷问,含泪拿下 60W offer

进击的王小二

java面试 大厂面试 java

翻车了,字节一道 Fragment面试题

小松漫步

面试 大厂面试 Android;

纵观移动云对象存储发展历程,也少不了 Apache APISIX 的能力加持

API7.ai 技术团队

API网关 企业案例 移动云 Apache APISIX

每秒创建百万文件,百度沧海·文件存储CFS推出新一代Namespace架构

百度大脑

人工智能

2021Android面试笔试总结!html5移动开发即学即用网盘

android 程序员 移动开发

2021京东Android面试真题,享学课堂android怎么样

android 程序员 移动开发

达摩院求解器升级 覆盖黑盒优化难题

Lily

10天拿到腾讯Android岗offer,内容太过真实

android 程序员 移动开发

2021Android大厂面试题来袭,Android性能优化推荐书

android 程序员 移动开发

第 21 章 -《Linux 一学就会》- 结构化命令case和for、while循环

学神来啦

同为aPaaS平台,华为云开天aPaaS与AppCube有何不同?

海比研究院

aPaaS

收藏!490家专精特新数智企业全名单:听听“小巨人”企业怎么说?

海比研究院

付费云存储,微信的登云梯还是蜀道难?

海比研究院

云存储

阿里云多个智物新品集体出道,持续加速产业智能化

Lily

2021Android进阶学习资料,动脑学院vip课程百度云

android 程序员 移动开发

2021年Android社招面试题,薪资翻倍

android 程序员 移动开发

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