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

队列方式

原理:

  • 按照请求发起顺序入队
  • 通过 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映射

选择哪种方法?

  • 取消之前的请求:最常用,适合大多数场景,尤其是搜索实时响应、快速分页加载等
  • 忽略过期响应:请求顺序不重要,只需要最新结果的场景
  • 队列方式:适用于严格控制请求顺序的场景(比如支付流程)。但可能导致用户体验下降(请求需要排队)
相关推荐
张元清5 小时前
React useInterval Hook:没有过期闭包的 setInterval (2026)
javascript·react.js
MXN_小南学前端5 小时前
React超长文本域中实现“返回顶部”浮动按钮
前端·javascript·react.js
Rain的Java大神之路6 小时前
线上接口负载满了如何解决
java·数据库·redis·后端·缓存·面试·架构
Dr.kangder7 小时前
嵌入式面试总结(十八)——JTAG调试
嵌入式硬件·面试·职场和发展·架构·嵌入式
Dr.kangder9 小时前
嵌入式面试总结(十六)——AMBA总线
面试·职场和发展·架构·嵌入式·虚拟化·总线
Dr.kangder9 小时前
嵌入式面试总结(十九)——内存泄露
单片机·算法·面试·职场和发展·架构·硬件架构
Dr.kangder10 小时前
嵌入式面试总结(十七)——DMA总线
面试·职场和发展·架构·嵌入式·总线
YIAN10 小时前
http无状态?State来展示!从基础路由到鉴权守卫,吃透 SPA 前端路由核心
前端·react.js
码匠许师傅11 小时前
【C++ 面试真题】聊聊 C++ 的虚函数和多态
java·c++·面试