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

队列方式

原理:

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

选择哪种方法?

  • 取消之前的请求:最常用,适合大多数场景,尤其是搜索实时响应、快速分页加载等
  • 忽略过期响应:请求顺序不重要,只需要最新结果的场景
  • 队列方式:适用于严格控制请求顺序的场景(比如支付流程)。但可能导致用户体验下降(请求需要排队)
相关推荐
伍哥的传说8 小时前
React 各颜色转换方法、颜色值换算工具HEX、RGB/RGBA、HSL/HSLA、HSV、CMYK
深度学习·神经网络·react.js
浪裡遊8 小时前
React Hooks全面解析:从基础到高级的实用指南
开发语言·前端·javascript·react.js·node.js·ecmascript·php
独行soc11 小时前
#渗透测试#批量漏洞挖掘#HSC Mailinspector 任意文件读取漏洞(CVE-2024-34470)
linux·科技·安全·网络安全·面试·渗透测试
小飞悟12 小时前
你以为 React 的事件很简单?错了,它暗藏玄机!
前端·javascript·面试
前端小盆友14 小时前
从零实现一个GPT 【React + Express】--- 【3】解析markdown,处理模型记忆
gpt·react.js
掘金安东尼14 小时前
技术解析:高级 Excel 财务报表解析器的架构与实现
前端·javascript·面试
天天扭码14 小时前
AI时代,前端如何处理大模型返回的多模态数据?
前端·人工智能·面试
阳火锅14 小时前
都2025年了,来看看前端如何给刘亦菲加个水印吧!
前端·vue.js·面试
Cacciatore->15 小时前
React 基本介绍与项目创建
前端·react.js·arcgis
摸鱼仙人~15 小时前
React Ref 指南:原理、实现与实践
前端·javascript·react.js