0.背景:
什么时候需要取消请求
1.前置知识:取消请求的底层原理
最底层的原理就是利用浏览器的XHR对象,主要有两个核心点:
1.利用XHR对象中的abort方法来已经发出的请求进行取消;
2.调用XHR.abort()之后,重置xhr.status状态值为0(响应成功状态值为4),避免走成功响应逻辑;
var xhr = new XMLHttpRequest(); // 1.新建XHR对象
xhr.open(url, methods); // 2.对xhr对象激活
xhr.send(body-params); // 3.发送
xhr.onreadystatechange = function() { // 4.监听响应状态
if (xhr.status === 4) {
}
}
总之,只要记住,取消已经发出请求的底层原理就是通过XHR.abort()函数,这个函数并不是把发出的http请求收回来,而是改变xhr对象的readyState状态值。
因为换个角度想,如果我们把接受到的响应数据不做任何处理的话,是不是相当于这个请求废掉了,也属于请求的取消。所以核心逻辑,就是通过abort函数将status状态改变置为0,避免走status === 4的逻辑,把请求返回的参数不对业务产生影响。当然最后都要给promise终态。
2.axios中取消请求功能的使用
axios的使用比较简单,主要就是通过axios.CancelToken.source()得到的内部的两个属性:token和cancel函数,其中token负责放在请求参数cancelToken值中,cancel负责调用取消请求
const { token, cancel } = axios.CancelToken.source(); // 1.获取token属性和cancel函数
axios.get('/api/user/info', {
cancelToken: token, // 2.赋值专门为取消请求设置的请求头(本质上是一个pending状态的promise)
// 其他参数
}).catch(() => {})
cancel('取消请求函数执行') // 3.执行取消请求功能函数cancel();
3.实现原理
// 这个构造函数产生的实例中,只有一个promise属性,其他都是内部私有变量。
// 那么
function CancelToken(executor) {
if (typeof executor !== 'function') { throw new TypeError('executor must be a function.'); }
var resolvePromise;
var token = this;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
executor(function cancel(message) {
if (token.reason) { return; }
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
请求头中cancelToken字段的作用
1.在发出请求的时候我们会在请求头添加一个cancelToken字段,其值为axios.cancelToken导出的token字段。而token的值就是一个cancelToken函数新建的实例对象(实例中函数promise属性和一些私有变量,暂时先不看私有变量,先重点看promise属性)。
2.下面axios在处理的时候会先去看config.cancelToken,即config请求头中是否存在cancelToken字段,如果存在则把这个字段值的promise属性(也就是cencelToken实例的promise属性)拿出来,如果cancelToken实例的promise属性对应的promise对象状态为resolved,执行then函数;
3.这个时候then主要处理两个任务:
(1) 负责最底层的XHR取消逻辑,执行XHR.abort(),后面的status逻辑也不会走(status = 0的逻辑也可以不管);
(2) 将整个axios对应的promise对象的状态改为rejected,因为我们在调用axios时候的时候一般是
axios.get(url).then((response) => {
// 处理收到响应数据逻辑
}).catch((err) => {
// 处理取消请求的逻辑,因为xhr.abort()和reject(cancel)执行了()
// 通过在token的then函数中执行axios对应promise的rejcet()函数,就不用走axios.get(url).then逻辑了,而是当成一个错误抛出来
})
token字段的作用:通过token中promise的状态来执行then,然后走底层的取消逻辑。而这个promise属性对应的promise对象状态什么时候改变,就是调用到处的另一个属性:cancel函数。
// 这里就是看我们有没有给请求附带cancelToken,如果带了就会走这里
if (config.cancelToken) {
// 还记得这里的promise知道是哪创建的吗?
config.cancelToken.promise.then(function onCanceled(cancel) {
// 两种情况,要么此时没请求,要么请求结束被清空为null,这里就不做处理,直接返回
if (!request) {
return;
}
// 这里调用了XMLHttpRequest.abort()
request.abort();
// 这里修改的是请求体自身promise的状态,而不是我们上文提到的那个promise,这里reject的是axios返回的那个promise状态,这样就和我们在业务中返回的promise对象同状态。
reject(cancel);
// 清空request,请求对象都不要了
request = null;
});
}
参考文献:www.1024sou.com/article/275…
上面提到axios是通过判断config中的cancelToken请求头是否存在,如果存在查看这个请求头的值是否为promise对象,如果是promise对象。那么当给这个promise对象添加then逻辑:当这个promise对象状态为resolved的时候,做两件事:
1.执行xhr.abort()取消请求(这个时候xhr对象的状态会重置为0),但是我们怎么不让他走业务逻辑呢?这就是第二步做的事
2.把axios对应的promise对象状态rejectd掉(这一步是来阻止走原来的业务逻辑的关键一步),因为我们的api书写是把业务逻辑放在响应成功的回调中。因此我们通过reject掉axios的promise就可以避免之前的业务逻辑。
下面我们来看一下,如何改变token对应promise状态
想一下,我们什么时候需要改变promise状态,改变这个promise状态意味着什么?显然,当我们想要在等待请求的期间取消请求的时候,我们就需要改变promise状态。
也就是说当我们执行cancel('error')这个取消函数的时候,就是我们需要改变promise状态的时候。因此cancel('error')要做的取消这个请求,本质上就是改变promise状态,两者等价。取消请求,就是要先触发改变promise状态这个条件,然后执行abort。
理解了这一步,我们再来看一下如何去改变。其实很简单,就是把promise构造函数的resolve函数抛出来,然后供cancel调用。如下:
我们调用的cancel就是source对象的cancel属性,这个属性值是一个函数,这个函数不是在内部定义的,而是通过CancelToken构造函数传过来的。
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
怎么传过来的:
// 这个构造函数产生的实例中,只有一个promise属性,其他都是内部私有变量。
// 那么
function CancelToken(executor) {
if (typeof executor !== 'function') { throw new TypeError('executor must be a function.'); }
var resolvePromise;
var token = this;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
executor(cancel);
// new CancelToken(function executor(c) { cancel = c; });会先执行executor函数,
// 这个executor函数是new的时候传的函数:function executor(c) { cancel = c; }
// 然后执行这个函数,把参数c赋值给变量cancel(这个cancel变量是在source函数中的,抛出来)
// 而这个c参数就是下面这个函数。
function cancel(message) {
if (token.reason) { return; }
token.reason = new Cancel(message);
resolvePromise(token.reason);
}
// 所以到这里,我们看出来调用cancel函数就是调用变量c函数,而c函数就是上面这个在cancelToken中定义的cancel函数,这个函数可以拿到promise的resolve钩子。同时给token的reason赋值,token就是CancelToken实例。
}
4.业务场景使用 当我们在快速切换tab的时候,我们需要将之前tab的请求取消,这样就不会出现显示结果错乱的情况。
let axiosTableSource = null; // step.1
private change(params: any) {
return new Promise((resolve, reject) => {
if (this.axiosTableSource) { // step.2
console.log('axiosTableSource');
this.axiosTableSource.cancel();
}
this.axiosTableSource = axios.CancelToken.source(); // step.3
apis('getDashboardCoreData', { ...params.params }, {}, {
cancelToken: this.axiosTableSource.token
})
.then((res: any) => {})
.catch((err: any) => {
this.$notify.error({
title: '提示',
message: err.msg || '图表数据获取失败'
});
});
});
}
1.这里切换tab的时候会触发change事件,然后this.axiosTableSource为null,接着往下走,此时第一次调用axios.CancelToken.source(),这个时候产生的cancel函数和token对象,假设此时API还在等待返回中,有切换了一次tab。 2.第二次触发change事件:
if (this.axiosTableSource) { // 存在,且保存的是上一个source对象
console.log('axiosTableSource');
this.axiosTableSource.cancel(); // 取消上一个source对象的
}
// 这里需要特别注意,取消上一个source对象的,怎么知道是上一个的,因为在上一次调用change时候,
// 执行了this.axiosTableSource = axios.cancelToken.source(),上一次执行的时候保存的是
// 上一次对应的对象,所以取消是通过token的promise对象的resolve触发执行
// axios通过判断config.cencelToken.promise.then(() => {
// xhr.abort()
// })
所以这里每次执行的时候,执行cancel()函数之后,一定要重新获取新的axios.CancelToken.source()对象,然后替换掉cancel函数和token值。
如果不替换,那么每次判断的时候token依然是第一次取消的promise状态,那么axios每次创建xhr对象,然后onreadystatechange事件监听到后,所走的逻辑判断的永远config中第一次给的那个promise对象,这样就会永远abort();
注意:虽然每次请求都会对应新的xhr对象,但是xhr对象的readystatechange事件的回调函数中,有一个逻辑是,通过请求配置参数中的cancelToken值,并通过这个token.then回调来执行abort,所以只有替换了,那么token对象才是新建的promise对象,否则永远是第一个promise对象。
4.axios重复请求取消
我们在业务中多多少少会存在一些由于重复请求引发的业务问题,这里我们分为两类问题:
4.1 绝对请求重复
绝对请求重复指的就是接口的路径,方法,参数都相同的请求重复,这里典型的一个业务问题就是:点击一个按钮之后,需要先调用一个权限接口,然后等在接口返回成功之后,需要打开一个弹窗。如果在业务代码中没有加上loading,那么用户就可以进行重复点击。这个时候两个相同的请求同时等待,那么都返回成功之后就会出现页面打开两个弹窗的问题。如果这个弹窗中是一个form表单,那么就可能造成重复提交。
一般来说,这个按钮是需要一个loading的,但是我们不排除编码过程中不会犯错,因此我们需要有一个兜底方案,这就是在axios中进行绝对重复请求取消,这种取消请求的代码一般写在axios拦截器中。
存在问题:做低代码的时候,很可能会出现不同组件发送相同的请求,如果全部抽离出来,会降低组件的复用性,所以一般会接受不同组件同时发送相同请求的情况,这种情况,如果还是用之前重复请求取消的方案,会造成只有一个组件有数据,其他组件没有数据。
4.2 功能请求重复
另一种就是tab切换问题,参数不同,但是我们需要取消上一个请求,防止tab展示的内容不对称,这种取消请求的代码一般写在业务组件中