axios CancelToken 取消频繁发送请求的用法和源码解析
前言
做一个Vue的项目时,遇到频繁切换标签的问题。由于不同标签请求的ajax的结果所需时间不同,点击不同标签时,响应时间最慢的数据会覆盖之前响应的数据,显示数据跟所点击标签不对应。当时为了处理这个问题,没想到好方法,只好控制在点击下一个标签前,必须等前一个标签的结果回来之后进行。
后来做API的统一管理时,看到前人写的axios的interceptor里有CancelToken这样一个东西,查了查资料,发现这个可以取消请求,踏破铁鞋无觅处,刚好可以用来处理之前遇到的频繁切换标签的问题。今作一记录,也好更好的理解这个功能。
述求
点击标签时,取消之前正在执行的请求,使得切换标签时,页面得到的是最后请求的结果,而不是响应最慢的结果。
用法
官方案例
- 使用 CancelToken.source 工厂方法创建 cancel token,像这样:
// CancelToken是一个构造函数,用于创建一个cancelToken实例对象 // cancelToken实例对象包含了一个promise属性,值为可以触发取消请求的一个promise const CancelToken = axios.CancelToken; // 执行source()得到的是一个包含了cancelToken对象和一个取消函数cancel()的对象 // 即 source = {token: cancelToken对象, cancel: 取消函数} const source = CancelToken.source(); // 在请求的配置中配置cancelToken,那么这个请求就有了可以取消请求的功能 axios.get('/user/12345', { cancelToken: source.token }).catch(function(thrown) { if (axios.isCancel(thrown)) { console.log('Request canceled', thrown.message); } else { // 处理错误 } }); axios.post('/user/12345', { name: 'new name' }, { cancelToken: source.token }) // 执行取消请求(message 参数是可选的) source.cancel('Operation canceled by the user.');
- 通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token
const CancelToken = axios.CancelToken; let cancel; axios.get('/user/12345', { cancelToken: new CancelToken(function executor(c) { // executor 函数接收一个 cancel 函数作为参数 // 把cancel函数传递给外面,使得外面能控制执行取消请求 cancel = c; }) }); // cancel the request cancel();
看起来有些晕,毕竟不知道里面是怎么运作的,稍后通过源码解析。这里简单解释就是,在请求中配置
cancelToken这个属性,是为了使得请求具有可以取消的功能;
cancelToken属性的值是一个
CancelToken实例对象,在它的
executor函数中提取出
cancel函数,执行这个
cancel函数来取消请求。
我的实例
点击标签,执行
getCourse函数。点击某个标签时,先取消之前的请求(如果之前的请求已完成,取消请求不会有任何操作)。效果是,页面显示的总是最后点击的标签对应的结果。
分两步,第一步,在get请求中配置
cancelToken属性,开启取消请求的功能,且在其属性值中将
cancel函数赋给
cancelRequest,使得外部可以调用
cancel函数来取消请求;第二步,在执行请求前,先取消前一次的请求。
import axios from 'axios' export default { data() { return { cancelRequest: null // 初始时没有请求需要取消,设为null } }, methods: { // 点击标签后发送请求的函数 getCourse() { const that = this // 2. 准备执行新的请求前,先将前一个请求取消 // 如果前一个请求执行完了,执行取消请求不会有其他操作 if (typeof that.cancelRequest === 'function') { that.cancelRequest() } // 这里配置请求的参数,略 let params = {} // 发送请求 axios.get('/api/app/course',{ params: params, cancelToken: new CancelToken(function executor(c) { // 1. cancel函数赋值给cancelRequest属性 // 从而可以通过cancelRequest执行取消请求的操作 that.cancelRequest = c }) }) } } }
一般API都会统一封装,所以,可以将请求封装起来
API
// /api/modules/course.js // _this为vue组件实例对象 export function getCourseReq(params, _this) { return axios.get('/api/app/course',{ params: params, cancelToken: new CancelToken(function executor(c) { // 1. cancel函数赋值给cancelRequest属性 // 从而可以通过cancelRequest执行取消请求的操作 _this.cancelRequest = c }) }) .then(res => {}) .catch(err => {}) }
组件
import { getCourseReq } from '@/apis/modules/course' methods: { getCourse() { // 2. 准备执行新的请求前,先将前一个请求取消 // 如果前一个请求执行完了,执行取消请求不会有其他操作 if (typeof this.cancelRequest === 'function') { this.cancelRequest() } // 这里配置请求的参数,略 let params = {} // 发送请求 getCourseReq(params, this) .then(res => {}) .catch(err => {}) } }
遇到的坑
一开始按照上述方法写好,但请求死活没有取消。一遍遍核对了变量名,调试输出信息,啥都对,就是没法取消。折腾半天,想起前人配置的axios的
interceptor里对每个请求配置了
cancelToken,目的是为了去掉重复的请求,但取消重复请求的代码段被注释掉了。把
cancelToken注释掉之后,终于雨过天晴。
axios.interceptors.request.use( config => { const request = JSON.stringify(config.url) + JSON.stringify(config.method) + JSON.stringify(config.data || '') // 这里配置了cancelToken属性,覆盖了原请求中的cancelToken config.cancelToken = new CancelToken(cancel => { sources[request] = cancel }) // if (requestList.includes(request)) { // sources[request]('取消重复请求') // } else { requestList.push(request) return config }, error => { return Promise.reject(error) } )
由于interceptor会在请求发送前做一些配置处理,这里把原请求中的
cancelToken覆盖了,那么即使原请求中执行原
cancelToken的
cancel函数,由于
cancelToken对象不同了,取消操作也就无效了。后面看了源码可以更明白为什么无效。
源码解析
根据前面的步骤,依次来看看各个源码是怎样。
首先,我们为请求配置
cancelToken属性,目的是使得请求具有能取消请求的功能,它的值是
CancelToken实例对象,那么
CancelToken是什么呢?
// 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对象}
executor
函数被执行,即cancel = c
执行,因此变量cancel
被赋值了,值就是CanelToken
内部的那个cancel
函数。
题外话,发现Promise其实也是传入executor,跟执行new Promise(executor)是一样的
CancelToken.source
CancelToken.source是一个函数,通过源码可以看到,执行
const source = CancelToken.source(),得到的是一个对象:
return { token: token, cancel: cancel };
包含一个
token对象,即
CancelToken实例对象,和一个
cancel函数。因此
CancelToken.source()函数的作用是生成
token对象和取得
cancel函数。
token对象是用于配置给
axios请求中的
cancelToken属性,
cancel函数是将来触发取消请求的函数。
如何执行取消请求的
万事俱备,就差如何执行取消请求了。这也是我一开始最疑惑的地方,一起来看看吧。
// axios/lib/adapters/xhr.js // 创建XHR对象 var request = new XMLHttpRequest() // 模拟当前ajax请求 request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true) // 定义取消请求promise对象的then函数 if (config.cancelToken) { // 如果配置了cancelToken属性 // 当promise为完成态时,这个then函数执行,即执行取消请求 config.cancelToken.promise.then(function onCanceled(cancel) { if (!request) { return; } // 取消ajax请求 request.abort(); reject(cancel); // Clean up request request = null; }); }
这是最终执行ajax请求的地方。
我们前面配置了
cancelToken = new CancelToken(executor),得到对象
{promise: 取消请求的Promise对象},因此上面
cancelToken.promise就是这个
promise。
我们知道,
Promise的
then函数是在
Promise对象处于
fullfilled时执行,这就是暗中的关联。执行
cancel函数,会将
Promise的状态置为
fullfilled,这里定义的
then函数就会执行,从而取消请求。
拦截器问题
之前遇到的问题,拦截器中配置的
cancelToken覆盖了请求中的
cancelToken,导致请求失效。原因是,拦截器重新定义后,
cancelToken属性值变成了新的token,
cancelToken.promise也就变成了新的
promise。
then函数是在拦截器之后、真正发送请求之前定义的,因此这时定义的
then函数,是对应这个新的Promise对象的,于是原来token的promise并没有then函数,那么执行原
cancel函数把原Promise对象置为了
fullfilled状态,但没有相应的then函数执行,有人发信号,没人执行,取消请求无效。
axios流程:定义请求 -> 请求拦截器 -> 发送请求 -> 响应拦截器 -> 接收响应
后语
一入源码深似海,看完真是受益匪浅,这里面的一些思想、做法,多可以学习借鉴。
- 【安卓网络请求开源框架Volley源码解析系列】初识Volley及其基本用法
- Volley源码解析(一)——发送请求与结束请求
- 【安卓网络请求开源框架Volley源码解析系列】初识Volley及其基本用法
- 【安卓网络请求开源框架Volley源码解析系列】初识Volley及其基本用法
- boa源码解析(1)-接收请求,发送html的流程
- axios 源码分析-取消请求
- 详解Axios 如何取消已发送的请求
- Picasso源码完全解析(六)--请求的取消、暂停、和恢复
- vuecli3使用axios发送请求到后端获取token详解
- Picasso源码完全解析(七)-- CleanupThread 取消请求
- 从 axios 源码中了解到的 Promise 链与请求的取消
- 【安卓网络请求开源框架Volley源码解析系列】初识Volley及其基本用法
- vue2.0项目实战 使用axios发送请求
- axios同时发送多个请求
- Python使用grequests(gevent+requests)并发发送请求过程解析
- springMVC源码分析--@SessionAttribute用法及原理解析SessionAttributesHandler和SessionAttributeStore
- axios设置token到请求头
- Android HandlerThread和IntentService用法和源码解析
- SpringMVC解析请求响应请求过程-源码分析
- LinkedList用法及源码解析