如何优雅的处理请求竞态问题

快速、频繁请求造成的数据混乱问题。

在 JavaScript 中处理请求竞态问题(即防止旧请求覆盖新请求结果)有以下几种优雅方案,按推荐程度排序:

方案 1:使用 AbortController (现代浏览器标准方案)

let controller = null;

async function fetchData(query) {
    // 取消上一个未完成的请求
    if (controller) controller.abort();

    controller = new AbortController();

    try {
        const response = await fetch(`/api?q=${query}`, {
            signal: controller.signal, // 绑定取消信号
        });
        const data = await response.json();
        // 处理数据(此时确保是最新请求)
        return data;
    } catch (err) {
        // 忽略被主动取消的请求错误
        if (err.name !== "AbortError") throw err;
    }
}

// 使用示例
searchInput.addEventListener("input", (e) => {
    fetchData(e.target.value);
});

方案 2:请求 ID 标记法 (兼容性好)

let currentRequestId = 0;

async function fetchData(query) {
    const requestId = ++currentRequestId;

    const response = await fetch(`/api?q=${query}`);
    const data = await response.json();

    // 只处理最新请求的结果
    if (requestId === currentRequestId) {
        // 渲染数据
    }
}

// 使用同上

方案 3:Promise 状态锁 (纯 Promise 实现)

let latestPromise = null;

async function fetchData(query) {
    const thisPromise = fetch(`/api?q=${query}`)
    .then(r => r.json());

    latestPromise = thisPromise;

    const result = await thisPromise;

    // 只处理最新的 Promise
    if (thisPromise === latestPromise) {
        // 处理数据
    }
}

方案 4:RxJS (响应式编程方案)

import { fromEvent } from "rxjs";
import { switchMap } from "rxjs/operators";

fromEvent(searchInput, "input")
    .pipe(
    switchMap((e) => fetch(`/api?q=${e.target.value}`).then((r) => r.json()))
)
    .subscribe((data) => {
    // 自动取消前序请求,只处理最新
});

方案 5:React Hooks 封装 (React 专属)

function useRaceSafeFetch() {
    const controllerRef = useRef();

    return async (url) => {
        // 取消上一个请求
        if (controllerRef.current) {
            controllerRef.current.abort();
        }

        const controller = new AbortController();
        controllerRef.current = controller;

        try {
            const res = await fetch(url, { signal: controller.signal });
            return res.json();
        } catch (err) {
            if (err.name !== "AbortError") throw err;
        }
    };
}

// 在组件中使用
function MyComponent() {
    const safeFetch = useRaceSafeFetch();

    const handleSearch = (query) => {
        safeFetch(`/api?q=${query}`).then((data) => {
            // 安全更新状态
        });
    };
}

最佳实践建议:

  1. 现代项目首选 AbortController - 原生支持且符合标准

  2. 框架集成

    • React:结合 useEffect + AbortController
    • Vue:在组件卸载时自动取消 (onUnmounted)
  3. 错误处理:务必检查 error.name === 'AbortError'

  4. 性能优化:添加防抖避免频繁取消请求

    // 配合防抖使用 (lodash.debounce)
    const debouncedFetch = debounce(fetchData, 300);
    

防抖 + AbortController 完整示例

function createRaceSafeFetcher() {
    let controller = null;
    let lastCall = 0;

    return async (query) => {
        // 防抖:300ms 内的请求视为同一批次
        const now = Date.now();
        if (now - lastCall < 300) {
            if (controller) controller.abort();
        }
        lastCall = now;

        controller = new AbortController();

        try {
            const res = await fetch(`/api?q=${query}`, {
                signal: controller.signal,
            });
            return res.json();
        } catch (err) {
            if (err.name !== "AbortError") throw err;
        }
    };
}

// 使用
const safeFetch = createRaceSafeFetcher();
searchInput.addEventListener("input", (e) => safeFetch(e.target.value));

选择方案的建议:

  • 浏览器项目:优先使用 AbortController
  • 兼容旧浏览器:请求ID标记法
  • React项目:自定义Hook封装
  • 复杂异步流:考虑RxJS

这些方案都能有效解决请求竞态问题,核心思路都是 "使旧请求失效""忽略旧响应"。实际项目中建议根据技术栈和浏览器支持情况选择最合适的方案。