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

队列方式

原理:

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

选择哪种方法?

  • 取消之前的请求:最常用,适合大多数场景,尤其是搜索实时响应、快速分页加载等
  • 忽略过期响应:请求顺序不重要,只需要最新结果的场景
  • 队列方式:适用于严格控制请求顺序的场景(比如支付流程)。但可能导致用户体验下降(请求需要排队)
相关推荐
JamSlade38 分钟前
流式响应 sse 系统全流程 react + fastapi为例子
前端·react.js·fastapi
徐同保40 分钟前
react useState ts定义类型
前端·react.js·前端框架
liangshanbo12151 小时前
React 19 vs React 18全面对比
前端·javascript·react.js
怪兽20141 小时前
Redis常见性能问题和解决方案
java·数据库·redis·面试
怪兽20142 小时前
Android多进程通信机制
android·面试
CptW4 小时前
手撕 Promise 一文搞定
前端·面试
腹黑天蝎座4 小时前
浅谈React19的破坏性更新
前端·react.js
第七种黄昏4 小时前
【前端高频面试题】深入浏览器渲染原理:从输入 URL 到页面绘制的完整流程解析
前端·面试·职场和发展
初听于你4 小时前
深入了解—揭秘计算机底层奥秘
windows·tcp/ip·计算机网络·面试·架构·电脑·vim
醉方休4 小时前
React 官方推荐使用 Vite
前端·react.js·前端框架