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

队列方式

原理:

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

选择哪种方法?

  • 取消之前的请求:最常用,适合大多数场景,尤其是搜索实时响应、快速分页加载等
  • 忽略过期响应:请求顺序不重要,只需要最新结果的场景
  • 队列方式:适用于严格控制请求顺序的场景(比如支付流程)。但可能导致用户体验下降(请求需要排队)
相关推荐
liangshanbo12158 小时前
面试题:React.memo 和 useMemo 的区别
前端·react.js·前端框架
怕浪猫17 小时前
RAG 面试 6 连问,从原理到优化全部覆盖
python·算法·面试
Interview Aid1121 天前
OpenAI SWE 面试流程
面试·职场和发展
飘尘1 天前
SVG和Canvas,前端里的两支“画笔”,用的时候怎么选择?
前端·javascript·面试
飘尘1 天前
从"算子"到"AI Infra":大模型背后看不见的那群人在忙什么
人工智能·算法·面试
ShineWinsu1 天前
对于Redis:主从复制的解析
linux·数据库·c++·redis·缓存·面试·主从复制
ShineWinsu1 天前
对于Redis:事务的解析
数据库·redis·mysql·缓存·面试·事务·acid
考研保研资料分享1 天前
电子信息保研经验合辑:复旦直博与北邮集成电路,夏令营、预推免和专业面试
数学建模·面试·职场和发展·信息与通信
(Charon)1 天前
【C++面试】堆内存与栈内存:string、vector和对象到底存在哪里
开发语言·c++·面试
太子釢1 天前
React 函数组件与 Hook 实践指南
前端·react.js