You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React搜索栏中Fetch请求图片的异步竞态问题

解决React中Fetch图片的异步竞态问题

问题根源

快速切换搜索条件时,上一次未完成的图片请求会继续执行,完成后会用旧数据覆盖新请求返回的状态,导致页面显示错误的图片数组(比如从返回11条结果的搜索切换到返回1条的搜索,旧请求完成后会把新的1条结果冲成11条)。

解决方案1:用AbortController主动取消旧请求

直接终止未完成的请求,从根源避免旧数据干扰。

1. 修改FetchImage,支持请求取消

const FetchImage = async ({id, token, removeToken, setUserError, signal}) => 
{
    try
    {
        const response = await fetch(`/api/artist/${id}/cover`, {
            method: "GET",
            headers: {
                'Content-Type': 'blob',
                'Authorization': 'Bearer ' + token
            },
            signal // 传入取消信号
        })
        if(!response.ok)
            throw new Error(response.status);

        const result = await response.blob();
        return URL.createObjectURL(result);
    }
    catch(error)
    {
        // 主动取消的错误直接忽略
        if(error.name === 'AbortError') return;
        
        if(error.message === "401")
        {
            LogMeOut({removeToken});
            setUserError("You have been logged out");
        }
        else
        {
            console.log("Unknown error " + error.message);
        }
    }
}

2. 更新FetchImages,传递取消信号并检查状态

async function FetchImages({songs, token, removeToken, setUserError, controller})
{
    const images = [];
    for(let i = 0; i < songs.length; i++)
    {
        // 如果请求已取消,直接终止循环
        if(controller.signal.aborted) break;
        
        const response = await FetchImage({
            id: songs[i].id, 
            token:token, 
            removeToken: removeToken, 
            setUserError: setUserError,
            signal: controller.signal
        });
        // 请求未取消且有结果才加入数组
        if(response) {
            images.push({key:i, Url:response});
        }
    }
    // 原排序逻辑多余(循环本身就是按顺序push),可以直接返回映射结果
    return images.map(obj => obj.Url);
}

3. 组件中管理请求控制器,触发新请求时取消旧请求

const [images, setImages] = useState([]);
// 用ref保存当前请求的控制器
const fetchControllerRef = useRef(null);

const fetchImages = async () => {
    // 取消上一次未完成的请求
    if(fetchControllerRef.current) {
        fetchControllerRef.current.abort();
    }
    // 创建新的控制器
    const controller = new AbortController();
    fetchControllerRef.current = controller;

    try {
        const newImages = await FetchImages({
            songs, 
            token, 
            removeToken, 
            setUserError,
            controller
        });
        // 只有当前请求未被取消时,才更新状态
        if(!controller.signal.aborted) {
            setImages(newImages);
        }
    } catch (err) {
        // 处理其他异常
    } finally {
        // 清理当前控制器(确保只清理最新的)
        if(fetchControllerRef.current === controller) {
            fetchControllerRef.current = null;
        }
    }
};

// 注意:依赖要加songs,搜索切换时songs变化就触发新请求
useEffect(() => {
    fetchImages();
    // 组件卸载时取消未完成的请求
    return () => {
        if(fetchControllerRef.current) {
            fetchControllerRef.current.abort();
        }
    };
}, [songs, token, removeToken, setUserError]);

解决方案2:用请求版本标记过滤旧结果

如果不想修改请求逻辑,可以用ref记录当前请求的版本号,只有最新版本的请求结果才更新状态。

const [images, setImages] = useState([]);
// 记录当前请求的版本号
const requestVersionRef = useRef(0);

const fetchImages = async () => {
    const currentVersion = ++requestVersionRef.current;
    
    const newImages = await FetchImages({
        songs, 
        token, 
        removeToken, 
        setUserError
    });
    
    // 只有当前版本是最新的,才更新状态
    if(currentVersion === requestVersionRef.current) {
        setImages(newImages);
    }
};

useEffect(() => {
    fetchImages();
}, [songs, token, removeToken, setUserError]);

额外优化提示

  • 原FetchImages中的排序逻辑是多余的,循环按i顺序执行,push的数组本身就是有序的,直接map即可。
  • 用URL.createObjectURL创建的对象会占用内存,建议在组件卸载或状态更新时,遍历旧的images调用URL.revokeObjectURL(url)释放资源。

内容的提问来源于stack exchange,提问作者Dmytro Avdieienko

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 10:55:34