壹 ❀ 引

axios,一个基于promise且对ajax进行了二次封装的http库,在提供了与promise类似的API便捷写法同时,它还有一大特点,便是支持取消http请求。当然取消请求并不是axios独有特性,它也只是对于XMLHttpRequest.abort()进行了内部封装。

我在如何做好一个基础的搜索功能?记一个因客户大数据量而导致的后发先至Bug一文中,对于文章问题给出的解决方案就有涉及到取消请求。虽然原生做法也能达到目的,但实际开发中我们不会用那么麻烦的写法,自然会借助三方http库,比如axios,而在这个库中,我们若需要取消请求,就得使用它的cancelToken,那么这个cancelToken到底是怎么做的呢?为什么一个简单的source.cancel调用接口调用就能做到取消?出于好奇,我简单阅读了axios对于cancelToken实现的相关源码,那么在这篇文章做个简单记录。

贰 ❀ 一个取消请求的例子

了解事情原貌最简单的做法就是写一个请求取消的例子,然后断点到axios中去阅读源码,因此我在本地项目准备了一个这样的例子,而本地项目请求接口容易跨域,为了解决这个问题,才有了React axios 使用 http-proxy-middleware 解决跨域问题小记这篇文章。所以这里我们就不多赘述,直接结合axios实现请求取消:

// 引用CancelToken
const CancelToken = axios.CancelToken;
// 调用CancelToken.source得到一个source实例,此实例包含token和cancel两个属性
const source = CancelToken.source();
// 请求接口时附带cancelToken:source.token,get与post有所区别,具体查看官方文档
axios.get('api/request', { cancelToken: source.token })
.catch(function (thrown) {
if (axios.isCancel(thrown)) {
alert(`Request canceled.${thrown.message}`);
}
});
// 通过source.cancel取消请求
source.cancel('Operation canceled by the user.');

上述代码中我们实现了一个简单接口取消请求,运行项目并打开控制台,发现并没发起request请求,且alert正常弹出。

而实现取消接口请求也比较简单,通过CancelToken.source()创建一个resoure实例:

// 单纯创建一个实例,不用于请求,让我们查看它
const source = CancelToken.source();
console.dir(source)

如图,此实例包含一个名为cancel的方法,接受一个message字段,也就你要取消请求时需要传递的理由,当然也可以不传。另外是一个token对象,它包含一个状态为pendingPromise对象(这个东西作用超级大,下文会解释这个Promise从哪来有什么用)。

取消请求的目的虽然达到了,可这几个方法像个黑盒,它里面到底发生了什么?没关系,让我们顺着上面请求取消的例子来一探究竟!

叁 ❀ cancelToken源码浅析

取消接口第一步创建resouce实例,它的源码在node_modules/axios/lib/cancel/CancelToken.js中可查看,代码如下:

/**
* Returns an object that contains a new `CancelToken` and a function that, when called,
* cancels the `CancelToken`.
*/
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};

当我们执行CancelToken.source()触发的就是上述代码,首先,我们可以看到此方法确实返回一个包含canceltoken两个属性的对象,让我们来看看执行过程。

首先,此方法创建了一个calcel变量,紧接着,又创建了一个token,而token的赋值结果是调用构造函数CancelToken得到,注意,此时我们在调用构造函数时,传递了一个function如下:

function executor(c) {
cancel = c;
}

你可以先不用思考它有什么用,就把这个函数理解成调用构造函数传递了一个实参,紧接着返回了一个包含上述两个属性的对象,对于source方法,它做的事情就这么简单,让我们紧接着看看构造函数CancelToken,代码如下:

/**
* A `CancelToken` is an object that can be used to request cancellation of an operation.
*
* @class
* @param {Function} executor The executor function.
*/
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
// 其实就外层保存resolve方法
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
}); var token = this;
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
} token.reason = new Cancel(message);
// 在这里调用resolve方法,用于改变Promise状态
resolvePromise(token.reason);
});
}

OK,对于构造函数CancelToken,它接受一个参数executor,而这个参数其实就是上面提到的executor方法。接着,函数内声明了一个名为resolvePromise的变量。然后就是构造器属性this.promise,既然是构造器属性,那么这里可以预先知道,source方法中得到的token上一定有这个promise属性(上面截图已经展示了token中有个Promise),而这个属性又由一个Promise构造器创建。

new Promise方法做的事情很简单,在内部将resolve赋值给外部变量resolvePromise,我们都知道Promise接受一个callback,而callback内部可以使用resolvereject两个方法,而上述所做的事仅仅是将Promise内部的resolve方法暴露出去,以达到在外层改变Promise状态的目的,比如:

// 外层定义一个变量用于保存promise内部的方法
let resolvePromise;
const p = new Promise((resolve, reject) => {
resolvePromise = resolve;
})
p.then((res) => {
console.log(res);// 我在外层改变promise状态
});
setTimeout(() => {
// 外层调用promise的resolve方法以达到改变promise状态目的
resolvePromise('我在外层改变promise状态')
}, 3000)

所以简单来说,通过这种做法我们将promise内部方法暴露出来,想在哪用就在哪用,大概如此。

让我们继续回到CancelToken方法,接着我们将this赋予给变量token,不要诧异,我们知道当new一个构造函数时,其实就是在隐式的给this赋值,然后返回这个this,而为了让这种绑定与返回更为可视化,常常有如下的写法:

function Fn(name){
const that = this;
that.name = name;
return that;
}

所以上面算是一种可视化做法,毕竟new CancelToken本身就是返回一个token并赋值给token,也相当于更好理解。

接着,我们执行了executor方法,它的参数又是一个函数,如下:

function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
// 在这里调用resolve方法,用于改变Promise状态
resolvePromise(token.reason);
}

同学们,还记得这个executor从哪来吗?它不就是new CancelToken时传递的函数吗:

function executor(c) {
cancel = c;
}

所以这个c其实就是上面传递给executorcancel方法,而cancel = c这一句,目的跟上面resolve赋值给外层一样,也是将定义在CancelToken内部的cancel暴露出去。

你可能有点混乱了,其实这里一共做了三次方法暴露,第一次我们将resolve暴露出去,目的是让cancel中可以通过resolvePromise来改变Promise状态;而这个cancel也不是在定义的地方使用,这里又做了第二次暴露,将cancel抛出去赋予给了source方法,紧接着source做了第三次方法暴露,当我们开发者调用source将得到canceltoken两个属性。大致流程如下图:

那么到这里我们解释了source方法是如何产生的这两个属性,以及这两个属性分别有什么用。

所以当我们调用接口时,传递了一个source.token其实就是给这次请求绑定一个状态是pendingPromise,它更像是一个开发,一旦我们调用source.cancel就会启动这个开发,告知axios要取消这个请求,怎么取消的呢?

让我们找到node_modules/axios/lib/adapters/xhr.js这个文件,文章开头就说了axios是对于ajax的封装,而ajax又是对于XMLHttpRequest那一套的封装,所以回归本质,在xhr.js中我们可以看到发起请求所有的准备代码都在这里,这里大致贴一点代码并补了注释,我删掉了部分对于本身意义不大的代码:

return new Promise(function dispatchXhrRequest(resolve, reject) {
// 准备请求附带数据
var requestData = config.data;
// 准备请求头
var requestHeaders = config.headers;
// 创建xhr对象
var request = new XMLHttpRequest(); // 授权相关
if (config.auth) {
var username = config.auth.username || '';
var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}
// 获取请求地址
var fullPath = buildFullPath(config.baseURL, config.url);
// 调用open,发起请求
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true); // 设置超时时间,你期望多久没反应就提示超时那就设置多少
request.timeout = config.timeout; // OK,监听state状态,
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
// 准备response数据体
var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request
}; settle(resolve, reject, response);
// 一旦请求结束,清空请求
request = null;
}; // 监听abort的处理
request.onabort = function handleAbort() {
}; // 监听error的处理
request.onerror = function handleError() {
}; // 监听超时后的处理
request.ontimeout = function handleTimeout() {
}; // 这里就是看我们有没有给请求附带cancelToken,如果带了就会走这里
if (config.cancelToken) {
// 还记得这里的promise知道是哪创建的吗?
config.cancelToken.promise.then(function onCanceled(cancel) {
// 两种情况,要么此时没请求,要么请求结束被清空为null,这里就不做处理,直接返回
if (!request) {
return;
}
// 这里调用了XMLHttpRequest.abort()
request.abort();
// 这里修改的是请求体自身promise的状态,而不是我们上文提到的那个promise
reject(cancel);
// 清空request,请求对象都不要了
request = null;
});
}
});

大家可以大致看看上述代码,这里我们抽离出最重要的一段:

// 这里就是看我们有没有给请求附带cancelToken,如果带了就会走这里
if (config.cancelToken) {
// 还记得这里的promise知道是哪创建的吗?
config.cancelToken.promise.then(function onCanceled(cancel) {
// 两种情况,要么此时没请求,要么请求结束被清空为null,这里就不做处理,直接返回
if (!request) {
return;
}
// 这里调用了XMLHttpRequest.abort()
request.abort();
// 这里修改的是请求体自身promise的状态,而不是我们上文提到的那个promise
reject(cancel);
// 清空request,请求对象都不要了
request = null;
});
}

config不用多说,其实就是我们使用axios发起请求时的配置,上文的例子也展示了,你要想取消请求,你就得给请求配置中加一个{ cancelToken: source.token },而这个token中其实就是一个状态为pendingPromise,目的是什么我们也很清楚了。这个Promise就为了是跟当前请求进行绑定,它就像一颗遥控炸弹,而炸弹的开关就是 source.cancel,当我们调用cancel时,就会执行下面这个方法:

function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
// 启动炸弹
resolvePromise(token.reason);
}

方法里就干一件事,将跟请求绑定在一起的Promise的状态给改成resolve,一旦修改完成,那是不是就得跑下面这段代码:

config.cancelToken.promise.then(function onCanceled(cancel) {
// 两种情况,要么此时没请求,要么请求结束被清空为null,这里就不做处理,直接返回
if (!request) {
return;
}
// 这里调用了XMLHttpRequest.abort()
request.abort();
// 这里修改的是请求体自身promise的状态,而不是我们上文提到的那个promise
reject(cancel);
// 清空request,请求对象都不要了
request = null;
});

毕竟你Promise状态变了,那我then就得执行,执行了干什么?request.abort()也就是XMLHttpRequest.abort()取消请求,成功把这个请求给引爆炸掉了!!!

abort干了什么呢?很遗憾,这个就彻彻底底是个黑盒了,是浏览器在帮我们处理,断点跟不进去,但通过MDN我们可以的值,当调用此方法时,如果请求已发出,那么该方法将中止请求。当一个请求被中止时,它 readyState被更改为 XMLHttpRequest.UNSENT(0) 并且请求的 status代码被设置为 0。

肆 ❀ 总

那么到这里,我们完整跟着请求代码把axios执行过程给讲了一遍,你会发现axios在内部其实就是创建一个名为token 的 Promise与一个改变Promise状态的方法cancel,当你需要取消一个请求时,那就把这个Promise与请求绑在一起,然后便可通过cancel改变请求上的Promise状态,从而达到取消请求的目的,不得不说这个三方库的作者那是真的牛妈妈给牛宝宝开门,牛到家了!!

另外,abort原生做法我测试发现若请求发出,是会取消请求,所以可以看到chrome控制台有红色的被取消的请求,而axios直接没请求,猜测应该是axios内部做了特殊处理,或者是我请求被取消的太快,导致根本没发出,至于这点我就没再去研究了。

var xhr = new XMLHttpRequest(),
method = "GET",
url = "https://developer.mozilla.org/";
xhr.open(method, url, true); xhr.send(); if (true) {
xhr.abort();
}

结合前面文章,我们就可以在简历中写通过axios.cancelToken解决XXX问题,若面试问你这个取消是怎么实现的。那真是不好意思,你算是撞到咱们的枪口上了,又可以愉快的跟面试官battle一番。OK,那么到这里,本文正式结束!!2点了!!睡觉!!

JS axios cancelToken 是如何实现取消请求?稍有啰嗦但超有耐心的 axios 源码分析的更多相关文章

  1. Flask框架(三)—— 请求扩展、中间件、蓝图、session源码分析

    Flask框架(三)—— 请求扩展.中间件.蓝图.session源码分析 目录 请求扩展.中间件.蓝图.session源码分析 一.请求扩展 1.before_request 2.after_requ ...

  2. Django框架深入了解_01(Django请求生命周期、开发模式、cbv源码分析、restful规范、跨域、drf的安装及源码初识)

    一.Django请求生命周期: 前端发出请求到后端,通过Django处理.响应返回给前端相关结果的过程 先进入实现了wsgi协议的web服务器--->进入django中间件--->路由f分 ...

  3. TOMCAT8源码分析——处理请求分析(下)

    前言 本文继续讲解TOMCAT的请求原理分析,建议朋友们阅读本文时首先阅读过<TOMCAT源码分析——请求原理分析(上)>和<TOMCAT源码分析——请求原理分析(中)>.在& ...

  4. Tomcat源码分析——请求原理分析(下)

    前言 本文继续讲解TOMCAT的请求原理分析,建议朋友们阅读本文时首先阅读过<TOMCAT源码分析——请求原理分析(上)>和<TOMCAT源码分析——请求原理分析(中)>.在& ...

  5. 源码分析Retrofit请求流程

    Retrofit 是 square 公司的另一款广泛流行的网络请求框架.前面的一篇文章<源码分析OKHttp执行过程>已经对 OkHttp 网络请求框架有一个大概的了解.今天同样地对 Re ...

  6. Flask框架cbv的写法、请求与响应、请求扩展、session源码分析、闪现

    本篇文章将会详细讲在flask框架如何写cbv.请求与响应.请求扩展.session源码分析.闪现等知识点. 目录 一.flask写CBV 二.请求与响应 三.session 四.闪现flash 五. ...

  7. Axios源码分析

    Axios是一个基于promise的HTTP库,可以用在浏览器和node.js中. 文档地址:https://github.com/axios/axios axios理解和使用 1.请求配置 { // ...

  8. Backbone.js源码分析(珍藏版)

    源码分析珍藏,方便下次阅读! // Backbone.js 0.9.2 // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. // Backbone ...

  9. Backbone.js 0.9.2 源码分析收藏

    Backbone 为复杂Javascript应用程序提供模型(models).集合(collections).视图(views)的结构.其中模型用于绑定键值数据和自定义事件:集合附有可枚举函数的丰富A ...

  10. 详解SpringMVC请求的时候是如何找到正确的Controller[附带源码分析]

    目录 前言 源码分析 重要接口介绍 SpringMVC初始化的时候做了什么 HandlerExecutionChain的获取 实例 资源文件映射 总结 参考资料 前言 SpringMVC是目前主流的W ...

随机推荐

  1. mybatis plus 获取新增实体的主键

    转载请注明出处: mybatis plus 新增实体对象调用的是 IService 接口中的 save 方法: default boolean save(T entity) { return SqlH ...

  2. .NET 5 开发WPF - 美食应用登录UI设计

    Demo演示: 你的时间宝贵,不想看啰嗦的文字,可直接拉到文末下载源码! 1. 新建项目 站长开发环境: VS 2019企业版 16.70 .NET 5 Preview 5 .NET 5 WPF 项目 ...

  3. ORA-65140: 无效的通用配置文件名称

    1.问题 CREATE PROFILE PM_Profile LIMIT SESSIONS_PER_USER 100 PASSWORD_LIFE_TIME 90; 在创建概要文件时,报错:ORA-65 ...

  4. 【RTOS】基于RTOS的嵌入式系统看门狗策略

    RTOS - high integrity systems 看门狗策略 Watchdog Strategies for RTOS enabled embedded systems 介绍 看门狗定时器就 ...

  5. 【ThreadX-USBX】Azure RTOS USBX概述

    Azure RTOS USBX是高性能USB主机,设备和移动(OTG)嵌入式堆栈.Azure RTOS USBX与Azure RTOS ThreadX完全集成,并且可用于所有ThreadX支持的处理器 ...

  6. [转帖]Windows设置WiFi走外网,有线网卡走内网。

    https://www.itblogcn.com/article/1844.html   文章目录 设置笔记本 WIFI 走外网,网线走内网: 1.查看路由表: 2.删除默认路由: 3.添加 WiFi ...

  7. [转帖]无需 zookeeper 安装 kafka 集群 (kakfa3.0 版本)

    https://xie.infoq.cn/article/7769ef4576a165f7bdf142aa3 一.kafka 集群实例角色规划 在 kafka3.0 中已经可以将 zookeeper ...

  8. [转帖]Python基础之文件处理(二)

    https://www.jianshu.com/p/7dd08066f499 Python基础文件处理 python系列文档都是基于python3 一.字符编码 在python2默认编码是ASCII, ...

  9. [转帖]ls命令

    ls(list) 命令可以说是Linux下最常用的命令之一 #ls -l;列出文件的详细信息 #ll 以上两个命令一样,ll是ls -l的简写 #ls -al;列出目录下的所有文件,包括以 . 开头的 ...

  10. [转帖]RPC 框架架构设计

    github地址:https://github.com/xiaojiesir/mini-rpc RPC 又称远程过程调用(Remote Procedure Call),用于解决分布式系统中服务之间的调 ...