本文详解如何在 React 搜索场景中避免因多次快速触发 fetch 导致的竞态条件(race condition),确保每次搜索只渲染对应请求的最新图片结果,而非前一次未完成请求的残留数据。
本文详解如何在 react 搜索场景中避免因多次快速触发 `fetch` 导致的竞态条件(race condition),确保每次搜索只渲染对应请求的最新图片结果,而非前一次未完成请求的残留数据。
在 React 应用中实现搜索栏图片加载时,一个常见却容易被忽视的问题是:多个并发异步请求未加控制,导致后发起的请求先返回、先更新状态,而先发起但响应较慢的请求后返回并错误地覆盖了正确结果——即所谓的“竞态条件”(Race Condition)。你描述的现象(输入 test11 却显示 test1 的 11 张图)正是典型表现。
根本原因在于:你的原始代码中 fetch() 函数被直接调用且未做请求取消或防重入处理;同时 useEffect 依赖项缺失,导致 fetch 函数在组件重渲染时可能捕获过期的 songs 值,进一步加剧状态错乱。
首先,将 fetch 定义为带稳定引用的回调函数,并严格声明其依赖项:
const [images, setImages] = useState<string[]>([]);const fetchImages = useCallback(async () => { try { const fetched = await FetchImages({ songs, token, removeToken, setUserError }); setImages(fetched); } catch (err) { console.error("Image fetch failed:", err); }}, [songs, token, removeToken, setUserError]); // ✅ 关键:所有闭包变量必须显式声明useEffect(() => { fetchImages();}, [fetchImages]); // ✅ 依赖稳定函数,避免无限循环
但这只是基础优化——它仍无法彻底解决竞态问题。更健壮的做法是引入 AbortController 或 请求取消标识。以下是推荐的增强版 FetchImages 实现:
async function FetchImages({ songs, token, removeToken, setUserError, signal // 新增 AbortSignal 参数}: { songs: { id: string }[]; token: string; removeToken: () => void; setUserError: (msg: string) => void; signal?: AbortSignal; // 支持中断}) { const images: { key: number; Url: string }[] = []; for (let i = 0; i < songs.length; i++) { if (signal?.aborted) { throw new Error("Fetch aborted due to new search"); } const response = await FetchImage({ id: songs[i].id, token, removeToken, setUserError, signal // 向下透传 }); images.push({ key: i, Url: response }); } return images .sort((a, b) => a.key - b.key) // ✅ 更简洁的数值排序写法 .map(obj => obj.Url);}
相应地,更新 FetchImage 以支持中断:
const FetchImage = async ({ id, token, removeToken, setUserError, signal}: { id: string; token: string; removeToken: () => void; setUserError: (msg: string) => void; signal?: AbortSignal;}) => { try { const response = await fetch(`/api/artist/${id}/cover`, { method: "GET", headers: { "Content-Type": "blob", Authorization: `Bearer ${token}` }, signal // ✅ 关键:传递 signal 实现请求中断 }); if (!response.ok) { if (response.status === 401) { LogMeOut({ removeToken }); setUserError("You have been logged out"); } throw new Error(`HTTP ${response.status}`); } const blob = await response.blob(); return URL.createObjectURL(blob); } catch (error) { if (error instanceof DOMException && error.name === "AbortError") { // 被主动取消,静默处理 return ""; } throw error; }};
最后,在搜索触发逻辑中(如 onChange 或 onSubmit),每次新搜索前创建新 AbortController:
const [controller, setController] = useState<AbortController | null>(null);const handleSearch = async () => { // 取消上一次请求 controller?.abort(); const newController = new AbortController(); setController(newController); try { const fetched = await FetchImages({ songs, token, removeToken, setUserError, signal: newController.signal }); setImages(fetched); } catch (err) { if (err.name !== "AbortError") { console.error("Search failed:", err); } }};
通过以上改造,你的搜索栏将具备真正的“所搜即所得”能力——无论用户快速连续输入多少次,最终展示的必然是最后一次有效请求的结果。