壹 ❀ 引
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
对象,它包含一个状态为pending
的Promise
对象(这个东西作用超级大,下文会解释这个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()
触发的就是上述代码,首先,我们可以看到此方法确实返回一个包含cancel
与token
两个属性的对象,让我们来看看执行过程。
首先,此方法创建了一个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
内部可以使用resolve
与reject
两个方法,而上述所做的事仅仅是将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
其实就是上面传递给executor
的cancel
方法,而cancel = c
这一句,目的跟上面resolve
赋值给外层一样,也是将定义在CancelToken
内部的cancel
暴露出去。
你可能有点混乱了,其实这里一共做了三次方法暴露,第一次我们将resolve
暴露出去,目的是让cancel
中可以通过resolvePromise
来改变Promise
状态;而这个cancel
也不是在定义的地方使用,这里又做了第二次暴露,将cancel
抛出去赋予给了source
方法,紧接着source
做了第三次方法暴露,当我们开发者调用source
将得到cancel
与token
两个属性。大致流程如下图:
那么到这里我们解释了source
方法是如何产生的这两个属性,以及这两个属性分别有什么用。
所以当我们调用接口时,传递了一个source.token
其实就是给这次请求绑定一个状态是pending
的Promise
,它更像是一个开发,一旦我们调用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
中其实就是一个状态为pending
的Promise
,目的是什么我们也很清楚了。这个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点了!!睡觉!!