一、如何设计一个取消请求的功能
取消请求是通过xhr.abort()这样实现的。但是不可能将xhr变量暴露出来,因此在ajax请求的内部必须有一个函数是取消请求的,通过这个函数取消请求达到隐藏xhr的目的。也可以直接是xhr.abort函数,显然这样就无法对取消请求的过程无法控制了。
function cancelRequest(){
if(xhr==null){
return
}
xhr.abort()
reject("请求取消")
xhr = null
}
接下来,这个函数怎么传递出去? 本来这个函数的控制权在封装的ajax函数里面,现在我们想把这个控制权转交给调用者。一种很巧妙的方式是通过函数传递出去,这要求初始化ajax请求时传过来一个函数去接收cancelRequest函数。
初始化时
var cancel = null
rquest({
...
cancelToken:function(c){
cancel = c
}
...
})
封装时
function rquest(config){
...
if(typeof config.cancelToken == "function"){
config.cancelToken(cancelRequest)
}
...
}
完整代码如下:
function xhr(config){
return new Promise((resolve,reject)=>{
var {url,data,headers,method,cancelToken} = config
var xhr = new XMLHttpRequest();
xhr.onreadystatechange=function(){
if (xhr.readyState!=4){
return
}
if (xhr.status === 0) {
return;
}
if(xhr.status >= 200 && xhr.status < 300){
//TODO::
resolve(xhr.responseText)
}else{
reject("请求异常")
}
xhr = null
}
xhr.onabort = function(){
if(!xhr){return}
reject("请求取消");
xhr = null
}
xhr.onerror = function(){reject("请求出错");xhr = null}
xhr.ontimeout = function(){reject("请求超时");xhr = null}
function cancelRequest(message){
if(xhr==null){
return
}
xhr.abort()
reject(message)
xhr = null
}
if(typeof cancelToken == "function"){
cancelToken(cancelRequest)
}
xhr.open(method,url,true);
xhr.send(data);
})
}
这里有一点需要注意的是当请求取消的时候,status为0,而且首先触发的onreadystatechange函数,为了不让程序在这里reject,需要加上判断if (xhr.status === 0)
。基本上取消请求的功能已经可以工作了,但是这么做有一个缺点,程序直接在onabort函数中reject出去了,无法包含请求取消的原因,因为xhr.abort()后面的reject无法工作。
二、请求分发
请求分发过程对取消请求的实现极为重要,细节见axios流程分析1.6节。前面那样实现的缺点
这个函数捕获到异常之后会判断是否是【请求取消】的异常,如果不是会尝试抛出【请求取消】的异常。而axios为取消请求封装的类中包含了这些功能。
三、axios取消请求解析
3.1 取消信息的封装,它包含可以识别出【取消请求】异常的属性__CANCEL__
function Cancel(message) {
this.message = message;
}
Cancel.prototype.toString = function toString() {
return 'Cancel' + (this.message ? ': ' + this.message : '');
};
Cancel.prototype.__CANCEL__ = true;
3.2 取消异常类型判断,对判断取消异常的封装
function isCancel(value) {
return !!(value && value.__CANCEL__);
};
3.3 取消请求核心逻辑
在最开始,我为了将http请求内的abort函数传递出去,是给cancelToken赋予一个函数类型的值,并调用它,把cancel函数作为它的参数。理解取消请求核心逻辑需要注意两点:
- CancelToken怎么和config.cancelToken传递信息
- http请求内部,abort函数怎么和CancelToken交互
因此这个部分需要做好和两边数据传递。
目前的情况是有三个函数:cancel、executor、abort,我希望cancel调用之后立即调用abort,可以按照下面的写法。在xhr函数中把真正能够取消请求的abort函数传递给CancelToken存起来,然后CancelToken初始化的时候把包装abort函数的cancel函数作为参数传递给外部的函数。
function CancelToken(executor){
this.real_cancel = null
var token = this;
executor(function cancel(message) {
if (token.reason) {
return;
}
token.reason = new Cancel(message);
token.real_cancel && token.real_cancel()
});
}
//xhr函数中
function xhr(config){
//...
if (config.cancelToken) {
config.cancelToken.real_cancel = function(){
if (!request) {
return;
}
request.abort();
request = null;
}
}
//...
}
这样做有一个缺陷,取消请求是异步进行,如果我想在取消请求之后立即做某件事是做不到的。所以axios是用promise来组织这三者之间的顺序。
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
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);
resolvePromise(token.reason);
});
}
//xhr 函数中
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
// reject(cancel);
// Clean up request
request = null;
});
}
首先把cancel函数传递给外部函数,只有cancel函数被调用了,CancelToken的promise才resolve,从而执行then里面的方法。在xhr函数中将取消请求的函数早就放到then里面去了,只是当时不会执行,等到cancel调用之后才执行。