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
相关产品推荐
相关产品推荐

