您的位置:首页 > 移动开发 > IOS开发

axios CancelToken 取消频繁发送请求的用法和源码解析

2020-02-04 18:35 585 查看

前言

做一个Vue的项目时,遇到频繁切换标签的问题。由于不同标签请求的ajax的结果所需时间不同,点击不同标签时,响应时间最慢的数据会覆盖之前响应的数据,显示数据跟所点击标签不对应。当时为了处理这个问题,没想到好方法,只好控制在点击下一个标签前,必须等前一个标签的结果回来之后进行。

后来做API的统一管理时,看到前人写的axios的interceptor里有CancelToken这样一个东西,查了查资料,发现这个可以取消请求,踏破铁鞋无觅处,刚好可以用来处理之前遇到的频繁切换标签的问题。今作一记录,也好更好的理解这个功能。

述求

点击标签时,取消之前正在执行的请求,使得切换标签时,页面得到的是最后请求的结果,而不是响应最慢的结果。

用法

官方案例

  1. 使用 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.');
  2. 通过传递一个 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
函数做了两件事:

  1. 创建一个
    Promise
    对象,且将这个对象赋值给
    promise
    属性,其
    resolve
    参数被暴露出来以备外部引用。
  2. 执行
    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流程:定义请求 -> 请求拦截器 -> 发送请求 -> 响应拦截器 -> 接收响应

后语

一入源码深似海,看完真是受益匪浅,这里面的一些思想、做法,多可以学习借鉴。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: