• 竞态问题 + axios 取消请求


    1,问题描述

    在日常开发中,可能会有以下场景:

    有多个 tab 页,每个 tab 是一个过滤条件,比如订单的状态:【已取消】,【待支付】,【已完成】。而 tab 对应的内容区域是同一个元素,显示列表。

    问题:当快速切换 tab 时,如果保证拿到当前 tab 的数据?

    比如当前 tab 是【已取消】,切换到【待支付】后快速切换到【已完成】,并且因为网络问题【已完成】的数据先回来,【待支付】的数据后回来,此时展示就出问题了。

    这就是一个请求竟态问题

    2,解决

    1,丢弃错误的结果

    通过标识符来保证,请求前和请求后的 id 相同。

    import { ref } from "vue";
    import { getArticleList } from "./api/apiList";
    
    let tabIndex = ref(0);
    const changeTabIndex = (index) => {
    	tabIndex.value = index;
    	// 其他业务...
    	getList();
    };
    
    const getList = async () => {
    	try {
    		const curIndex = tabIndex.value;
    		const data = await getArticleList();
    		if (curIndex !== tabIndex.value) {
    			return;
    		}
    		console.log("获取正确的数据");
    	} catch (error) {
    		console.log(error);
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    // ./api/apiList
    import axios from "./index";
    
    export const APIS = {
    	GET_ARTICLE_LIST: "/api/list",
    };
    export const getArticleList = () => {
    	return axios.get(APIS.GET_ARTICLE_LIST);
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2,取消之前的请求

    当切换新 tab 页时,之前的请求就没有必要继续了,为了提升效率,可以取消掉已经发送的请求。

    axios 官方文档 有介绍如何取消发起的请求,代码很简单:

    // 引入 axios 后,就会有这个构造函数。
    const controller = new AbortController();
    
    axios
    	.get("/foo/bar", {
    		signal: controller.signal,
    	})
    	.then(function (response) {
    		//...
    	});
    // 取消请求
    controller.abort();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    CancelToken 的方式,axios 在 v0.22.0 就已经弃用了,应该用上面的新方式。

    这样的话,我们可以在请求拦截器中增加判断。

    import axios from "axios";
    
    let needCancelRequests = {};
    
    // 添加请求拦截器
    axios.interceptors.request.use(
    	function (config) {
    		const controller = new AbortController();
    		config.signal = controller.signal;
            // 这里的标识符比较简单,可根据具体业务增加复杂性来保证唯一。
    		needCancelRequests[config.url] = controller;
    		return config;
    	},
    	function (error) {
    		return Promise.reject(error);
    	}
    );
    
    // 添加响应拦截器
    axios.interceptors.response.use(
    	function (response) {
    		// 删除已完成的
    		if (needCancelRequests[response.config.url]) {
    			delete needCancelRequests[response.config.url]
    		}
    		return response;
    	},
    	function (error) {
    		// 删除已完成的
    		if (needCancelRequests[error.config.url]) {
    			delete needCancelRequests[error.config.url]
    		}
    		if ((error.name = "CanceledError")) {
    			console.log("已取消请求");
    		}
    		return Promise.reject(error);
    	}
    );
    
    export { needCancelRequests };
    export default axios;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    在页面中使用

    以上面的例子来说,修改这个方法,在请求之前停掉上个 tab 页发起的请求:

    import { needCancelRequests } from "./api/index";
    
    const changeTabIndex  = (index) => {
        if (needCancelRequests[APIS.GET_ARTICLE_LIST]) {
    		needCancelRequests[APIS.GET_ARTICLE_LIST].abort();
    	}
        tabIndex.value = index;
        getList();
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3,其他

    1,有了以上的逻辑,我们也可以做到切换页面(路由)时,取消上个页面还在请求中的请求。

    具体实现:

    // 添加请求拦截器
    axios.interceptors.request.use(
    	function (config) {
    		const controller = new AbortController();
    		config.signal = controller.signal;
    		// 这里增加更多的标识,到时在全局路由守卫中,循环过滤上个路由的所有请求,逐个取消即可。
    		needCancelRequests[config.url] = {
                'controller': controller,
                router: 'xxx'
            };
    		return config;
    	},
    	function (error) {
    		return Promise.reject(error);
    	}
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    4,潜在的问题

    上面使用的 needCancelRequests[config.url] 作为标识符,但其实问题很大,上面只是介绍的最简单的情况。更复杂的情况如下:

    1. 多个接口的方法名相同,但请求方式不同
    2. config.url包括 query 参数的,所以在页面中手动取消时,就不能使用已经定义好的不带的 query 参数的 url 作为 key。就是这里:
    const changeTabIndex  = (index) => {
        // APIS.GET_ARTICLE_LIST 并不带 query 参数,但在请求拦截器中的 key 却带着 query 参数。
        if (needCancelRequests[APIS.GET_ARTICLE_LIST]) {
    		needCancelRequests[APIS.GET_ARTICLE_LIST].abort();
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    所以,具体情况还需要具体分析。比如可以把方法名也拼上,或把自定义参数(应该定义一个字典保存)作为配置项传入,

    export const getArticleList = () => {
        // get 请求的第2个参数就是配置项,在拦截器中可以取到。
    	return axios.get(APIS.GET_ARTICLE_LIST, { a: 1123 });
    };
    
    • 1
    • 2
    • 3
    • 4

    以上。

  • 相关阅读:
    C语言:详细说明线程的同步操作:互斥,无名信号量,条件变量,以及需要注意的一些问题
    Semantic Kernel 入门系列:💬Semantic Function
    数据被加密?.locked1勒索病毒的简单解决方法
    计算机网络通信
    搜维尔科技:基于人体工学和多形态手势识别的交互式人机交互系统研究与设计
    Appium+python+unittest搭建UI自动化框架!
    「PAT甲级真题解析」Advanced Level 1008 Elevator
    Kafka的Java客户端-生产者
    【OpenCV4】使用 magnitude() 函数计算幅值(c++)
    LeetCode·每日一题·1678.设计 Goal 解析器·双指针
  • 原文地址:https://blog.csdn.net/qq_40147756/article/details/137974078