写点什么

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

评论

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

pip安装报错:No such file or directory 'conda-forge' 没有那个文件或目录 'conda-forge'

Geek_7ubdnf

Python

MobaXterm链接linux虚拟机报错Network error: Connection refused

Geek_7ubdnf

mobaxterm

万字技术干货 |YMatrix 高性能时序数据库引擎的技术实践

YMatrix 超融合数据库

性能优化 Clickhouse 时序数据 超融合数据库 YMatrix

如何使用 rust 写内核模块

阿里巴巴中间件

阿里云 rust 云原生

华为云数据库GaussDB(for Redis),如何为人们日常生活保驾护航

科技怪授

数据库

EMQX在Kubernetes中如何进行优雅升级

EMQ映云科技

运维 物联网 IoT 节点 企业号 1 月 PK 榜

10月15日 Apache IoTDB & SeaTunnel 联合 Meetup | 讲座回顾

Apache IoTDB

教你3步完成阿里云物联网平台公共实例到企业实例的迁移——实践类

阿里云AIoT

安全 物联网 物联网安全 技术标签

软件测试/测试开发 | Jenkins 多任务关联

测试人

软件测试 持续集成 jenkins 自动化测试 测试开发

华为云大数据赋能北港集团发展创新,数字化转型成效显著!

IT科技苏辞

软件测试/测试开发 | 如何安装Jenkins 插件?

测试人

软件测试 持续集成 jenkins 自动化测试 测试开发

Vue基础知识和案例展示

Geek_7ubdnf

Vue

雄关漫道真如铁,而今迈步从头越 | 挥别2022,再战2023!

BoCloud博云

博云

闲话 dubbogo 社区

阿里巴巴中间件

阿里云 云原生 dubbo

使用 WebP 图像加速WEB加载

devpoint

Web 前端开发 网站优化 WebP

TensorBoard的使用

Geek_7ubdnf

tensorflow

玩转物联网平台规则引擎-设备数据与业务应用高效协同——实践类

阿里云AIoT

安全 物联网 物联网安全 技术标签

浪潮云分布式云ICP加速千行百业羽化创新

云计算

Frp流量隐匿对抗排查

领创集团Advance Intelligence Group

内网渗透 Frp 流量编排

今年大促季,阿里云容器服务有哪些技术和应用新突破?

阿里巴巴中间件

阿里云 容器 云原生

一文读懂Go Http Server原理

捉虫大师

Go HTTP 1月月更

深入思考 Schema 管理的几个基本问题

观远数据

软件测试/测试开发 | Jenkins通过什么方式报警?

测试人

软件测试 持续集成 jenkins 自动化测试 测试开发

ScrollView(滚动条)

梦笔生花

android ScrollView(滚动条) 滚动效果

EfficientFormer 提升速度的同时保持性能,使 ViT 在移动端成为可能

Zilliz

还在高投入自建物联网平台?教你如何节省三分之二成本——实践类

阿里云AIoT

安全 物联网 物联网安全 技术标签

华为云大数据解决方案赋能金融行业发展,打造5G智慧银行营业厅

IT科技苏辞

数据治理:数据集成的关键技术

用友BIP

看这篇就够了丨基于Calcite框架的SQL语法扩展探索

袋鼠云数栈

拿下中国信通院多项测评的华为云数据库,究竟有多牛?

与时俱进的时代

MarkDown文件插入图片并上传GitHub

Geek_7ubdnf

markdown

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