写点什么

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

评论

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

飞桨大模型分布式训练技术

Baidu AICLOUD

飞桨 百度百舸 AI 大底座

源码解析Collections.sort ——从一个逃过单测的 bug 说起 | 京东云技术团队

京东科技开发者

排序算法 源码解读 企业号 7 月 PK 榜 Collections.sort

基于因果关系知识库的因果事件图谱构建

汀丶人工智能

人工智能 自然语言处理 知识图谱

Java中的JDBC的使用方法有哪些?

java易二三

Java 编程 程序员 计算机

高性能、高扩展、高稳定:解读 EasyMR 大数据组件自定义可扩展能力

袋鼠云数栈

大数据 大数据组件

广东省《5A物理抗菌纺织品》团体标准颁布

极客天地

基于Hologres向量计算与大模型免费定制专属聊天机器人

阿里云大数据AI技术

人工智能 hologres

618技术揭秘 - 大促弹窗搭投实践 | 京东云技术团队

京东科技开发者

前端 弹窗 xview 企业号 7 月 PK 榜

全彩LED显示屏品质由什么决定

Dylan

媒体 广告 科技 LED LED显示屏

免费SAFe大规模敏捷管理工具

顿顿顿

Scrum #敏捷开发 敏捷开发管理工具 SAFe框架

Java基础之IO流

java易二三

Java 编程 程序员 io 计算机

Coral Finance 将为 Zepoch 节点空投,Nautilus生态空投季开启

西柚子

详解!视频直播源码布谷科技平台搭建开发:录制功能

山东布谷科技

软件开发 视频直播 源码搭建 短视频直播源码 视频录制

Sugar BI:大模型时代的智能 BI

Baidu AICLOUD

BI 数据智能

内部Java内存模型与硬件层面内存模型的关系是什么?

java易二三

Java 编程 程序员 硬件 计算机

2023开源数据库排行榜发布,“新晋黑马”瀚高IvorySQL跻身三十强

极客天地

支付宝小程序云李铮:科技赋能,敏捷增长

TRaaS

支付宝小程序 小程序云开放 蚂蚁

火山引擎DataLeap的Data Catalog系统公有云实践 (上)

字节跳动数据平台

大数据 数据中台 数据治理 数据安全 企业号 7 月 PK 榜

中文人物关系知识图谱(含码源):中文人物关系图谱构建、数据回标、基于远程监督人物关系抽取、知识问答等应用.

汀丶人工智能

人工智能 nlp 知识图谱 智能问答

超强阵容!HarmonyOS极客马拉松2023专家评审团来袭!

HarmonyOS开发者

HarmonyOS

万字长文浅析配置对MySQL服务器的影响 | 京东物流技术团队

京东科技开发者

MySQL 数据库 服务器 企业号 7 月 PK 榜 MySQL服务器

一种配置化的数据脱敏与反脱敏框架实现 | 京东云技术团队

京东科技开发者

数据安全 脱敏 数据脱敏 企业号 7 月 PK 榜

KaiwuDB 资深解决方案专家周幸骏:打造核心时序引擎,释放数据新价值

KaiwuDB

时序数据 KaiwuDB

自主AI代理:未来的生产力引擎

互联网工科生

人工智能 AI代理

REST API 版本控制:高效管理

Apifox

程序员 RESTful API REST API API 测试

浅聊一下大模型

鲸品堂

大模型训练 大模型

明晚直播:可重构计算芯片的AI创新应用分享!

飞桨PaddlePaddle

人工智能 百度 paddle 飞桨 百度飞桨

科研类项目核算的“法、术、器”(二)

用友BIP

项目管理 科研项目

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