4

axios的config中提供了一个cancelToken的属性来取消请求。

用法

用法一

利用CancelToken.source方法,它会返回带有CancelToken实例和cancel方法的对象。将CancelToken实例赋值给config.cancelToken。在需要取消请求时调用cancel方法就会取消带有与这个cancel相对应的token的所有请求。

var CancelToken = axios.CancelToken;
var source = CancelToken.source();
axios.get('/user/12345', {//get请求在第二个参数
    cancelToken: source.token
}).catch(function(thrown) {
});
axios.post('/user/12345', {//post请求在第三个参数
    name: 'new name'
}, {
    cancelToken: source.token
});
source.cancel('不想请求了');

用法二

通过实例化一个CancelToken对象,并传入一个回调函数,赋值cancel方法。

var CancelToken = axios.CancelToken;
var cancel;
axios.get('/user/12345', {
    cancelToken: new CancelToken((c)=>cancel=c)//实例化一个CancelToken对象,赋值cancel方法
}).catch(function(thrown) {
});
cancel()

第一种用法只是在第二种用法上多包了一层,本质差不多

原理分析

// axios/lib/cancel/CancelToken.js

'use strict';

var Cancel = require('./Cancel');

function CancelToken(executor) {
    if (typeof executor !== 'function') {
        throw new TypeError('executor must be a function.');
    }
    /**
    * 定义一个将来能执行取消请求的promise对象,当这个promise的状态为完成时(fullfilled),
    * 就会触发取消请求的操作(执行then函数)。而执行resolve就能将promise的状态置为完成状态。
    * 这里把resolve赋值给resolvePromise,就是为了在这个promise外能执行resolve而改变这个promise的状态
    * 注意这个promise对象被赋值给CancelToken实例的属性promise,将来定义then函数就是通过这个属性得到promise
    */
    var resolvePromise;
    this.promise = new Promise(function promiseExecutor(resolve) {
        resolvePromise = resolve;
    });
    /**
    * 将CancelToken实例赋值给token
    * 执行executor函数,将cancel方法传入executor,
    * cancel方法可调用resolvePromise方法,即触发取消请求的操作
    */
    var token = this;
    executor(function cancel(message) {
        if (token.reason) {
            // 取消已响应 返回
            return;
        }
        token.reason = new Cancel(message);
        // 这里执行的就是promise的resolve方法,改变状态
        resolvePromise(token.reason);
  });
}

CancelToken.prototype.throwIfRequested = function throwIfRequested() {
    if (this.reason) {
        throw this.reason;
    }
};

// 这里可以看清楚source函数的真面目
CancelToken.source = function source() {
    var cancel;
    var token = new CancelToken(function executor(c) {
        // c 就是CancelToken中给executor传入的cancel方法
        cancel = c;
    });
    return {
        token: token,
        cancel: cancel
    };
};

module.exports = CancelToken;

CancelToken

CancelToken是一个构造函数,通过new CancelToken()得到的是一个实例对象,创建时它只有一个属性promise, 它的值是一个能触发取消请求的Promise对象。

token = new CancelToken(executor function) ===> { promise: Promise对象 }

执行CancelToken函数做了两件事:

创建一个Promise对象,且将这个对象赋值给promise属性,其resolve参数被暴露出来以备外部引用。
执行executor函数,将内部定义的cancel函数作为参数传递给executor

var token = this;
var cancel = function (message) {
    if (token.reason) {
        // 取消已响应 返回
        return;
    }
    token.reason = new Cancel(message);
    // 这里执行的就是promise的resolve方法,改变状态
    resolvePromise(token.reason);
}
executor(cancel);

当执行

let cancel
token = new CancelToken(function executor(c) {
    cancel = c;
});

token值为{promise: Promise对象}
cancel就是CanelToken内部的那个cancel函数。

CancelToken.source

CancelToken.source是一个函数,通过源码可以看到,执行const source = CancelToken.source(),得到的是一个对象:

return {
    token: token,
    cancel: cancel
};

包含一个token对象,即CancelToken实例对象,和一个cancel函数。因此CancelToken.source()函数的作用是生成token对象和取得cancel函数。token对象是用于配置给axios请求中的cancelToken属性,cancel函数是将来触发取消请求的函数。

config.cancelToken.promise

在cancelToken.promise的回调中调用abort中止请求

if (config.cancelToken) {
    // Handle cancellation
    config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
            return;
        }
        request.abort();//xmlHttpRequest对象的abort中止请求
        reject(cancel);
        // Clean up request
        request = null;
    });
}

Cancel

Cancel用来存储cancel方法传入的取消原因

executor(function cancel(message) {//cancel传入原因
        if (token.reason) {
            // 取消已响应 返回
            return;
        }
        token.reason = new Cancel(message);token.reason存储原因
        // 这里执行的就是promise的resolve方法,改变状态
        resolvePromise(token.reason);
  });
//Cancel.js
'use strict';

/**
 * A `Cancel` is an object that is thrown when an operation is canceled.
 *
 * @class
 * @param {string=} message The message.
 */
function Cancel(message) {
  this.message = message;
}

Cancel.prototype.toString = function toString() {
  return 'Cancel' + (this.message ? ': ' + this.message : '');
};

Cancel.prototype.__CANCEL__ = true;

module.exports = Cancel;

axios.isCancel()

axios.isCancel来判断该请求是不是通过cancel手动取消的

'use strict';

module.exports = function isCancel(value) {
  return !!(value && value.__CANCEL__);
};

cancelToken用法实战

问题

一个聊天项目遇到这样的一个问题,客户在用户列表中切换用户,每次切换都会去获取用户的基本信息和之前的聊天记录并渲染到页面上。如果频繁切换,可能导致页面要响应很多请求,并且如果前一次切换的请求在最后一次切换请求后面返回还会导致页面上渲染的数据不对。

思考

虽然可以通过防抖的方式解决,但是每次切换延迟一段时间用户体验又不好,所以想到了使用cancelToken的方式来解决。

基本思路

1、每次请求生成一个cancelToken,也可以自己传入cancelToken。然后根据url和method,将对应的cancel方法存起来
2、当有同样的请求过来后,通过url和method拿出cancel方法并调用,这样就可以取消前一次还没返回的相同请求,页面也不会响应。保证页面一段时间内只会响应最后一次重复请求。

相关代码

//cancelToken.js
// 声明一个 Map 用于存储每个请求的标识 和 取消函数
const pending = new Map()
/**
 * 添加请求
 * @param {Object} config 
 */
export const addPending = (config) => {
  const url = [
    config.method,
    config.url
  ].join('&')
  config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
    if (!pending.has(url)) { // 如果 pending 中不存在当前请求,则添加进去
      pending.set(url, cancel)
    }
  })
}
/**
 * 移除请求
 * @param {Object} config 
 */
export const removePending = (config) => {
  const url = [
    config.method,
    config.url
  ].join('&')
  if (pending.has(url)) { // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
    const cancel = pending.get(url)
    cancel(url)
    pending.delete(url)
  }
}
/**
 * 清空 pending 中的请求(在路由跳转时调用)
 */
export const clearPending = () => {
  for (const [url, cancel] of pending) {
    cancel(url)
  }
  pending.clear()
}
import {addPending,removePending} from "./cancelToken"//引入cancelToken
// 创建axios实例
const service = axios.create({
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'X-Requested-With': 'XMLHttpRequest'
  }
})

// request拦截器
service.interceptors.request.use(config => {
  removePending(config) // 在请求开始前,对之前的请求做检查取消操作
  addPending(config) // 将当前请求添加到 pending 中
  return config
}, error => {
  // Do something with request error
  console.log(error) // for debug
  Promise.reject(error)
})

// respone拦截器
service.interceptors.response.use(
  response => {
    removePending(response) // 在请求结束后,移除本次请求
    Promise.relove(response)
  },
  error => {
    if (axios.isCancel(error)) {//处理手动cancel
      console.log('这是手动cancel的')
    }
    return Promise.reject(error)
  }
)

star
64 声望3 粉丝

小菜鸟成长记录


引用和评论

0 条评论