壹 ❀ 引

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. docker 镜像管理之 overlay2 最佳实践

    1. Docker 镜像 Docker 镜像是个只读的容器模板,它组成了 Docker 容器的静态文件系统运行环境 rootfs,是启动 Docker 容器的基础. Docker 镜像是容器的静态视角 ...

  2. Clickhouse执行处理查询语句(包括DDL,DML)的过程

    Clickhouse执行处理查询语句(包括DDL,DML)的过程 总体过程 启动线程处理客户端接入的TCP连接: 接收请求数据,交给函数executeQueryImpl()处理: executeQue ...

  3. SpringMVC - 加载静态资源

    静态资源过滤 spring-config.xml <!-- 3,(1)让Spring MVC不处理静态资源 .(2)加载静态资源,也称为资源过滤 --> <mvc:default-s ...

  4. [转帖]win10多网卡指定ip走某个网卡的方案

    https://zhuanlan.zhihu.com/p/571614314 我的电脑上有两个网卡,一个网卡A(网线),一个是网卡B(WIFI). 需求:网卡A和网卡B是不同的网络,网卡A已经把338 ...

  5. [转帖]tidb的分区表

    https://docs.pingcap.com/zh/tidb/v6.5/partitioned-table 分区类型 本节介绍 TiDB 中的分区类型.当前支持的类型包括 Range 分区.Ran ...

  6. [转帖]针对容器的nginx优化

    针对容器的nginx优化 本篇文章介绍了 Nginx 在容器内使用遇到的CPU核数获取问题以及对应的解决方法. 回顾上篇文章:TCP 半连接队列和全连接队列 背景 容器技术越来越普遍,很多公司已经将容 ...

  7. [转帖]python中对配置文件的读写操作

    https://juejin.cn/post/6844903586963390471 python内置的configparser模块能非常方便的对配置文件进行操作,常见的配置文件有*.ini和*.co ...

  8. [转帖]Elasticsearch-索引性能调优

    1:设置合理的索引分片数和副本数 索引分片数建议设置为集群节点的整数倍,初始数据导入时副本数设置为 0,生产环境副本数建议设置为 1(设置 1 个副本,集群任意 1 个节点宕机数据不会丢失:设置更多副 ...

  9. [转帖] jq实现json文本对比

      原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处. 简介# 近期,为了给一个核心系统减负,组内决定将一些调用量大的查询接口迁移到另一个系统,由于接口逻辑比较复杂,为了保 ...

  10. Redis IO多线程的简要测试结果

    Redis IO多线程的简要测试结果 摘要 最近想简单确认一下IO多线程的对吞吐量的提升情况. 正好手头有鲲鹏的机器, 所以想直接进行一下验证 顺便用一下4216 进行一下对比. 发现 在CPU核心比 ...