前端发送多次请求,怎么保证请求参数与请求对应?

队列方式

原理:

  • 按照请求发起顺序入队
  • 通过 isProcessing 标志防止并行处理
ts 复制代码
import axios, { AxiosRequestConfig } from 'axios';

interface QueuedRequest<T = any> {
  params: T;
  resolve: (value: unknown) => void;
  config?: AxiosRequestConfig;
}

class RequestQueue<T = any> {
  private queue: QueuedRequest<T>[] = [];
  private isProcessing = false;

  async add(params: T, config?: AxiosRequestConfig): Promise<any> {
    return new Promise((resolve) => {
      this.queue.push({ params, resolve, config });
      // 请求入队时,判断当前是否有请求正在执行
      if (!this.isProcessing) this.process();
    });
  }

  private async process() {
    this.isProcessing = true;
    while (this.queue.length > 0) {
      const { params, resolve, config } = this.queue.shift()!;
      try {
        const response = await axios({
          method: 'GET',
          url: '/api/data',
          params,
          ...config
        });
        resolve({ data: response.data, originalParams: params });
      } catch (error) {
        resolve({ error, originalParams: params });
      }
    }
    this.isProcessing = false;
  }
}

export default new RequestQueue();

适用场景:

  • 需要严格控制请求顺序(比如支付流程)

缺点:

  • 但是这种方式有个缺陷,如果当前请求很费时间,后续还存在其他请求,会导致用户体验下降

当然也可能存在前面的请求失败了,则终止所有请求的场景,可以如下拓展:

tsx 复制代码
import axios, { AxiosRequestConfig, CancelTokenSource } from 'axios';

class AtomicRequestQueue {
  private queue: Array<{ 
    task: () => Promise<any>; 
    config?: AxiosRequestConfig 
  }> = [];
  private globalController: AbortController | null = null;
  private isProcessing = false;
  private hasFailed = false; // 全局失败标记

  // 添加请求到队列
  addRequest(task: () => Promise<any>, config?: AxiosRequestConfig) {
    this.queue.push({ task, config });
    if (!this.isProcessing) this.process();
  }

  // 处理队列
  private async process() {
    this.isProcessing = true;
    this.globalController = new AbortController(); // 创建全局终止控制器
    
    for (const request of this.queue) {
      if (this.hasFailed) break; // 已有失败则终止
      
      try {
        // 注入全局终止信号
        const res = await request.task();
        console.log('请求成功:', res);
      } catch (error) {
        if (!axios.isCancel(error)) { 
          // 非主动取消的错误触发全局终止
          this.hasFailed = true;
          this.globalController.abort(); 
          console.error('请求失败触发全局终止', error);
        }
        break;
      }
    }
    
    this.resetQueue();
  }

  // 重置队列状态
  private resetQueue() {
    this.queue = [];
    this.globalController = null;
    this.isProcessing = false;
    this.hasFailed = false;
  }

  // 外部终止接口
  abortAll() {
    this.globalController?.abort();
    this.resetQueue();
  }
}

// 全局单例
export const atomicQueue = new AtomicRequestQueue();

取消之前的请求

通过 axios 的 AbortController 取消过期请求

tsx 复制代码
import { useEffect, useRef } from 'react';
import axios, { AxiosRequestConfig } from 'axios';

export function useCancelableRequest() {
  const abortRef = useRef<AbortController>();

  const fetchData = async (config: AxiosRequestConfig) => {
    // 取消前一个未完成的请求
    if (abortRef.current) {
      abortRef.current.abort();
    }
    
    const controller = new AbortController();
    abortRef.current = controller;
    
    try {
      const response = await axios({
        ...config,
        signal: controller.signal
      });
      return response.data;
    } catch (err) {
      if (!axios.isCancel(err)) throw err;
    }
  };

  useEffect(() => {
    return () => abortRef.current?.abort();
  }, []);

  return fetchData;
}

使用场景:

  • 适用于绝大部分场景,比如搜索框实时获取、连续分页加载等

缺点:

  • 需要手动控制管理器

忽略过期响应

为每个请求分配一个唯一的标识符(例如,递增的序列号或时间戳),并在响应中包含该标识符。前端只处理具有最新标识符的响应,忽略过期的响应。

tsx 复制代码
import { useState, useEffect, useRef } from 'react';
import axios, { AxiosRequestConfig } from 'axios';

export function useRaceSafeFetch() {
  const versionRef = useRef(0);

  const safeFetch = async <T,>(config: AxiosRequestConfig): Promise<T> => {
    const currentVersion = ++versionRef.current;
    
    try {
      const response = await axios(config);
      // 版本校验:只处理最新请求的响应
      if (currentVersion === versionRef.current) {
        return response.data;
      }
      throw new Error('EXPIRED_RESPONSE');
    } catch (error) {
      if (axios.isCancel(error) || error.message === 'EXPIRED_RESPONSE') {
        return Promise.reject(new Error('请求已被更新'));
      }
      throw error;
    }
  };

  useEffect(() => {
    return () => {
      versionRef.current = -1; // 组件卸载时标记所有请求过期
    };
  }, []);

  return safeFetch;
}

使用场景:

  • 适用于请求响应顺序不重要,只需要最新结果的场景

缺点:

  • 需要维护key映射

选择哪种方法?

  • 取消之前的请求:最常用,适合大多数场景,尤其是搜索实时响应、快速分页加载等
  • 忽略过期响应:请求顺序不重要,只需要最新结果的场景
  • 队列方式:适用于严格控制请求顺序的场景(比如支付流程)。但可能导致用户体验下降(请求需要排队)
相关推荐
蒟蒻小袁1 小时前
力扣面试150题--实现Trie(前缀树)
leetcode·面试·c#
程序员爱钓鱼1 小时前
Go同步原语与数据竞争:原子操作(atomic)
后端·面试·go
潘小磊2 小时前
高频面试之10 Spark Core & SQL
sql·面试·spark
小满zs7 小时前
Zustand 第五章(订阅)
前端·react.js
谢尔登8 小时前
【React】常用的状态管理库比对
前端·spring·react.js
一嘴一个橘子11 小时前
获取DOM
react.js
GISer_Jing11 小时前
JWT授权token前端存储策略
前端·javascript·面试
拉不动的猪11 小时前
es6常见数组、对象中的整合与拆解
前端·javascript·面试
蒟蒻小袁11 小时前
力扣面试150题--单词接龙
算法·leetcode·面试
GISer_Jing11 小时前
Vue Router知识框架以及面试高频问题详解
前端·vue.js·面试