axios取消功能详解

axios提供 CancelToken 方法可以取消正在发送中的接口请求。

官方提供了两种方式取消发送,第一种方式如下:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');

第二种方式如下:

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // An executor function receives a cancel function as a parameter
    cancel = c;
  })
});

// cancel the request
cancel();

官方实现取消功能的文件存放在 /lib/cancel/CancelToken.js

虽然代码不多,但是第一次看真是一头雾水,下面就来抽丝剥茧,一步步还原里面的实现逻辑。

分析

两种方式都调用了 CancekToken 这个构造函数,我们就先从这个构造函数开始。

分析:

第一种方式:

  • CancekToken 提供一个静态方法sourcesource方法返回tokencancel方法

第二种方式:

  • CancekToken 接收一个回调函数作为参数,回调函数接收cancel取消方法

第二种方式更容易入手,我们可以先实现构造函数CancekToken,再考虑第一种方式静态方法source的实现。

简易版 axios

首先我们写个简易版的axios,方便我们后面的分析和调试:

知识点:PromiseXMLHttpRequest

function axios(url,config){
  return new Promise((resolve,reject)=>{
    const xhr = new XMLHttpRequest();
    xhr.open(config.method || "GET",url);
    xhr.responseType = config.responseType || "json";
    xhr.onload = ()=>{
      if(xhr.readyState === 4 && xhr.status === 200){
        resolve(xhr.response);
      }else{
        reject(xhr)
      }
    };
    xhr.send(config.data ? JSON.stringify(config.data) : null);
  })
}

CancelToken

第二种方式中,我们可以看到 CancelToken 在配置参数cancelToken中实例化:

axios.get('/user/12345', {
  cancelToken: new CancelToken
});

所以在axios中,我们也会根据配置中是否包含cancelToken来取消发送:

function axios(url,config){
  return new Promise((resolve,reject)=>{
    const xhr = new XMLHttpRequest();
    ...
    if(config.cancelToken){
      // 如果存在 cancelToken 参数
      // xhr.abort() 终止发送任务
      // reject() 走reject方法
    }
    ...

回到配置参数,CancelToken接受一个回调函数作为参数,参数包含取消的cancel方法,我们初始化CancelToken方法如下:

function CancelToken(executor){
  let cancel = ()=>{};
  executor(cancel)
}

回到官方例子,例子中参数cancel方法被赋值给当前环境的cancel变量,于是当前环境cancel变量指向CancelToken方法中的cancel函数表达式。

let cancel;
axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    cancel = c; // 指向CancelToken中的 cancel 方法
  })
});

接下来cancel方法一旦被执行,就能触发请求终止(发布订阅)。

这里官方源码巧妙的使用了Promise链式调用的方式实现,我们给CancelToken方法返回一个Promise方法:

function CancekToken(executor){
  let cancel = ()=>{};
  const promise = new Promise(resolve => cancel = resolve);
  executor(cancel);
  return promise;
}

接下来只要用户执行cancel方法,配置参数cancelToken获得的Promise方法就能响应了:

let cancel;
axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    cancel = c; // 指向CancelToken中的 cancel 方法
  })
});
// 执行
+ cancel("canceled request");

这里可以把 cancel 理解成 Promise.resolve

axios中响应cancel方法:

function axios(url,config){
  return new Promise((resolve,reject)=>{
    const xhr = new XMLHttpRequest();
    ...
    if(config.cancelToken){
+       config.cancelToken.then(reason=>{
+        xhr.abort();
+        reject(reason);
      })
    }
    ...

** 关键点是把 Promise.resolve 从函数内部抽出来,巧妙的实现了异步分离 **

到了这里,第二种方法的取消功能就基本实现了。

CancekToken.source

source作为 CancekToken 提供的静态方法,返回tokencancel 方法。

cancel方法跟前面的功能是一样的,可以理解成局部环境里面声明好cancel再抛出来。

我们再来看看第二种方式 token 在配置中的使用:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token // token 返回的是CancelToken实例
})

根据前面的配置我们可以知道 source.token 实际上返回的是 CancelToken 实例。

了解 source 方法需要返回的对象功能后,就可以轻松实现source方法了:

CancekToken.source = function(){

  let cancel = ()=>{};
  const token = new CancekToken(c=>cancel = c);

  return {
    token,
    cancel
  }
}

axios.isCancel

通过上的代码我们知道,取消请求会走reject方法,在Promise中可以被catch到,不过我们还需要判断catch的错误是否来自取消方法,这里官方提供了isCancel方法判断:

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (error) {
  // 判断是否 取消操作
  if (axios.isCancel(error)) {}
});

在js中我们可以通过instanceof判断是否来自某个构造函数的实例,这里新建Cancel方法来管理取消发送的信息:

function Cancel(reason){
  this.message = reason;
}

CancekToken.source 返回的cancel方法通过函数包装,实例化一个Cancel作为取消参数:

CancekToken.source = function(){
  
-  let cancel = ()=>{};
-  const token = new CancekToken(c=>cancel = c);
  
+ let resolve = ()=>{};
+  let token = new CancekToken(c=>resolve = c);

  return {
    token,
-    cancel,
+   cancel:(reason)=>{
+     // 实例化一个小 cancel,将 reason 传入
+     resolve(new Cancel(reason))
+   }
  }
}

最终Promise.catch到的参数来自实例Cancel,就可以很容易的判断error是否来自Cancel了:

function isCancel(error){
  return error instanceof Cancel
}
// 将 `isCancel` 绑定到 axios
axios.isCancel = isCancel

最后,官方还判断了CancelToken.prototype.throwIfRequested,如果调用了cancel方法,具有相同cancelToken配置的ajax请求也不会被发送,这里可以参考官方代码的实现。

全部代码

最后是全部代码实现:

function Cancel(reason) {
  this.message = reason
}

function CancekToken(executor) {
  let reason = null
  let resolve = null
  const cancel = message => {
    if(reason) return;
    reason = new Cancel(message);
    resolve(reason)
  }
  const promise = new Promise(r => (resolve = r))
  executor(cancel)
  return promise
}

CancekToken.source = function() {
  let cancel = () => {}
  let token = new CancekToken(c => (cancel = c))

  return {
    token,
    cancel
  }
}

const source = CancekToken.source()

axios('/simple/get', {
  cancelToken: source.token
}).catch(error => {
  if (axios.isCancel(error)) {
    console.log(error)
  }
})

source.cancel('canceled http request 1')

let cancel
axios('/simple/get', {
  cancelToken: new CancekToken(c => {
    cancel = c
  })
}).catch(error => {
  if (axios.isCancel(error)) {
    console.log(error)
  }
})
cancel('canceled http request 2')

function axios(url, config) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    xhr.open(config.method || 'GET', url)
    xhr.responseType = config.responseType || 'json'

    if (config.cancelToken) {
      config.cancelToken.then(reason => {
        xhr.abort()
        reject(reason)
      })
    }

    xhr.onload = () => {
      if (xhr.readyState === 4 && xhr.status === 200) {
        resolve(xhr.response)
      } else {
        reject(xhr)
      }
    }
    xhr.send(config.data ? JSON.stringify(config.data) : null)
  })
}

axios.isCancel = function(error) {
  return error instanceof Cancel
}

es6简易版本的实现:

export class Cancel {
    public reason:string;
    constructor(reason:string){
        this.reason = reason
    }
}
export function isCancel(error:any){
    return error instanceof Cancel;
}
export class CancelToken {
    public resolve:any;
    source(){
        return {
            token:new Promise(resolve=>{
                this.resolve = resolve;
            }),
            cancel:(reason:string)=>{
                this.resolve(new Cancel(reason).reason)
            }
        }
    }
}