axios取消请求原理

427 阅读9分钟

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展示的内容不对称,这种取消请求的代码一般写在业务组件中