axios-4-取消请求

402 阅读4分钟

取消请求

取消请求可能不是多见的情况,一般情况下都没有处理
但是比如有三个tab,默认展示第一个tab,本应该展示第一个tab的内容,由于此时用户网速较慢,数据并未及时返回,同时用户点击了第二个tab,那么这时候用户点击的是tab2,但是看到的确还是tab1的内容
这会对用户造成困扰,所以需要取消不需要的请求

axios中,取消请求api

axios 使用 取消请求

从 v0.22.0 开始,Axios 支持以 fetch API 方式—— AbortController 取消请求

const controller = new AbortController(); 
axios.get('/foo/bar', {
  signal: controller.signal
}).then(function(response) { //... }); // 取消请求 controller.abort()

(原先可以使用CancelToken,CancelToken 官网在v0.22已被废弃,推荐使用上文的AbortController)

AbortController

通过 AbortController() 构造函数来创建一个 controller 实例
然后通过 AbortController.signal 属性获取到它的关联对象AbortSignal 的引用

当 fetch request 初始化后,将 AbortSignal 作为一个选项传入请求的选项参数中(如下 {signal})。这将 signal,controller 与 fetch 请求关联起来,允许我们通过调用 AbortController.abort() 来取消 fetch 请求,正如下第二个事件监听器所示

var controller = new AbortController();
var signal = controller.signal;

var downloadBtn = document.querySelector('.download');
var abortBtn = document.querySelector('.abort');

downloadBtn.addEventListener('click', fetchVideo);

abortBtn.addEventListener('click', function() {
  controller.abort();
  console.log('Download aborted');
});

function fetchVideo() {
  ...
  fetch(url, {signal}).then(function(response) {
    ...
  }).catch(function(e) {
    reports.textContent = 'Download error: ' + e.message;
  })
}

简单来讲,使用AbortController 实例中的signal来对fetch进行绑定关联,调用实例abort方法,即可把关联的请求执行取消操作

axios 源码关于取消请求

在核心方法xhr方法中,有这么一个判断

var xhr = function xhrAdapter(config) {
  ...
 return new Promise(resolve,reject)=>{
  var request = new XMLHttpRequest();
  if (config.cancelToken || config.signal) {

        onCanceled = function(cancel) {
          // 已经取消过了
          if (!request) {
            return;
          }

          // 有 cancel 并且有 cancel type 
          reject(!cancel || cancel.type ? new CanceledError_1(null, config, request) : cancel);
          request.abort();
          request = null;
        };

        config.cancelToken && config.cancelToken.subscribe(onCanceled);

        if (config.signal) {
          config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
        }
      }
      ...
   }
 }

config.signal监听abort事件,回调onCanceled函数,如果外面执行了controller.abort();,onCanceled函数就会执行,在onCanceled内部执行request.abort方法同时把request变为null

graph TD
config.signal --监听--> abort
abort --回调-->onCanceled
onCanceled--执行-->reject & request.abort & request=null
click request.abort "https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/abort"

最终还是到了requset.abort上,它属于XMLHttpRequest静态方法,是XMLHttpRequest的取消请求方法

使用 CancelToken 构造函数取消请求不建议🐱

虽然说CancelToken已经不建议使用,但是它的思想还是挺有趣的

const CancelToken = axios.CancelToken; 
    let cancel;
    axios.get('/user/12345', {
       cancelToken: new CancelToken(function executor(c) {
       // executor 函数接收一个 cancel 函数作为参数
       cancel = c;
     }) 
}); // 取消请求 
cancel();

CancelToken的实例中接收一个函数executorexecutor中的参数由外界变量cancel接收,只要执行cancel函数即可取消请求

分析CancelToken

  1. 首先是一个函数,可以接受一个函数executor
  2. executor的函数注入实参c
  3. c应该也是一个函数
  4. 执行c函数就可以执行上文的取消请求方法

CancelToken 源码分析

function CancelToken(executor){
     var resolvePromise;

    this.promise = new Promise(function promiseExecutor(resolve) {
      resolvePromise = resolve;
    });

    // 保留 this
    var token = this;
    
    this.promise.then(function(cancel) {

      if (!token._listeners) return;

      var i = token._listeners.length;

      while (i-- > 0) {
        token._listeners[i](cancel);
      }

      token._listeners = null;
    });
    
    executor(function cancel(message, config, request) {
      if (token.reason) {
        // Cancellation has already been requested
        return;
      }

      token.reason = new CanceledError_1(message, config, request);

      resolvePromise(token.reason);
    });
}

CancelToken 是一个高阶函数,接收executor函数,使用变量 resolvePromise拿到一个promiseresolve引用,resolvePromise执行,promisethen方法也会执行,那么then中都this._listeners都会执行

graph TD
this.promise=newPromise; 
resolvePromise=newPromise.resolve

graph TD
CancelToken ----|接收参数executor| B(executor) %%内部函数cancel
B(executor)--执行-->resolvePromise执行
resolvePromise执行--触发-->this.promise.then执行
this.promise.then执行--执行-->this._listeners遍历执行
简化版CancelToken
function CancelToken(executor){
 var resolvePromise;

 this.promise = new Promise(resolve=>{
  resolvePromise = resolve
 })

 this.promise.then((res)=>{
  console.log("触发then函数",res)
 })

  executor(function innerCancel(message){
    resolvePromise(message);
  })
}


let cancel;
 CancelToken(function outerCancel(c){
  cancel = c
})

cancel("Rng")
  1. 外部 变量cancel接收CancelToken的回调函数中的实参c,

  2. 此时outerCancel函数对应的是executor

  3. executor的实参innerCancel对应的是实参c,

  4. c又赋值给变量cancel,此时innerCancelcancel是等价的

  5. 执行cancel就是执行innerCancel

  6. 执行innerCancel执行resolvePromise

  7. 执行resolvePromise触发promisethen方法

现在变成了执行then方法的每一个_listeners方法
this指向CancelToken 是通过subscribe进行不断增加_listeners的函数数组

CancelToken.prototype.subscribe

CancelToken.prototype.subscribe = function subscribe(listener) {
    if (this.reason) {
      listener(this.reason);
      return;
    }

    if (this._listeners) {
      this._listeners.push(listener);
    } else {
      this._listeners = [listener];
    }
  };

在上文中的xhr方法中,已经执行了一次

function xhr(){
    ...
    if (config.cancelToken || config.signal){
        config.cancelToken && config.cancelToken.subscribe(onCanceled);
    }
    ...
}

最后还是落到了onCanceled身上

image.png

所以使用了数组这种结构,可以同时取消多个使用同一个cancelToken关联的请求


CancelToken.source

这也可以取消请求,是上文的变体

const CancelToken = axios.CancelToken; 
const source = CancelToken.source();
axios.post('/user/12345',
  { name: 'new name' }, {
  cancelToken: source.token
})
source.cancel('Operation canceled by the user.');

CancelToken.source源码

 CancelToken.source = function source() {
    var cancel;
    var token = new CancelToken(function executor(c) {
      cancel = c;
    });
    return {
      token: token,
      cancel: cancel
    };
  };

这个cancel是那个promiseresolve方法的引用,执行cancel,相当于执行promise source.token还是那个cancelToken的实例



写在最后

通过此次源码分析,学到了

  1. 高阶函数executor
  2. promise的灵活运用,resolve触发,就自动执行then方法,体现了发布订阅的这种设计模式的优势
  3. fetch 和xhr的取消请求,他们触发模式不一样,fetch是真正的sigal关联,axios是模拟这种写法
  4. source 的这个变体,提供了很大的灵活性,用的都是一个CancelToken.source(),用的也是同一个CancelToken.token,一次可以取消多个请求,使用构造函数的这种写法,可以对单个请求进行单独处理

time:2022/10/3