ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

并发渲染下的副作用陷阱:effect 清理与竞态

2026/9/4 22:05:36 拓冰建站 浏览量
并发渲染下的副作用陷阱:effect 清理与竞态 并发渲染下的副作用陷阱effect 清理与竞态在 React 18 开启 Concurrent Mode并发渲染之后前端组件的生命周期模型发生了一个翻天覆地的变化Render渲染协调阶段变成了可中断、可恢复、甚至可重复执行的非连续过程。在过去同步渲染的世界里组件的render - commit - effect是一条直线。很多开发者习惯在组件内部写一些带有隐式副作用的代码如在函数体顶层发起请求、或者在useEffect中发起异步获取却不写 Cleanup 函数。但在并发渲染与 Strict Mode严格模式双挂载的双重检验下这些不良习惯会瞬间暴露出严重的线上 Bug网络请求竞态覆盖最新数据、定时器重复累积、事件监听泄漏导致内存暴涨、以及幽灵状态回流。要守住 React 架构的稳定性必须彻底厘清并发副作用的底层机理与防御策略。陷阱 1异步请求竞态Race Conditions最典型的场景是用户在标签页Tab之间快速连续点击切换用户点击 Tab A发起请求 Request A用户立即点击 Tab B发起请求 Request B由于网络波动Request B 在 100ms 内返回页面展示 Tab B 的内容随后卡顿的 Request A 在 500ms 后才慢吞吞返回执行setData(resA)——页面内容被错误地篡改回了 Tab A 的历史数据用户操作: 点击 Tab A ────── 立即点击 Tab B 网络请求: Request A 发出 ──┐ Request B 发出 ──┐ │ │ (100ms 快速完成) │ ▼ │ 页面渲染 Tab B (正确) │ (500ms 慢速到达) ▼ 页面被覆盖为 Tab A ❌ (严重竞态 Bug!)生产级防御方案AbortController Cleanup 取消在useEffect的清理函数中立即中止未完成的请求import { useState, useEffect } from react; function UserProfile({ userId }: { userId: string }) { const [profile, setProfile] useStateProfileData | null(null); useEffect(() { // 实例化原生 AbortController 控制器 const controller new AbortController(); const { signal } controller; async function loadData() { try { const response await fetch(/api/users/${userId}, { signal }); const data await response.json(); setProfile(data); } catch (err: any) { if (err.name AbortError) { // 请求被主动中止静默忽略不产生多余状态变更 return; } console.error(加载失败, err); } } loadData(); // 核心清理当 userId 改变或组件卸载时立即中止上一轮未完成的网络请求 return () { controller.abort(); }; }, [userId]); return div{profile ? profile.name : Loading...}/div; }陷阱 2函数体顶层的“幽灵副作用”在并发渲染下一个组件在最终 Commit 挂载到真实 DOM 之前它的函数体可能因为时间切片Time Slicing或低优中断而被 React反复执行多次Render 阶段重入。❌ 致命错误代码在函数体直接操作外部状态// 绝对禁止的反模式在函数组件体直接修改外部可变变量 let renderCount 0; function CounterDisplay() { // 错误在并发中断恢复时该行可能被调用 3 次但实际只 Commit 挂载了 1 次 renderCount; // 错误在函数体内直接发起全局埋点打点 analytics.track(VIEW_COUNTER); return divCount: {renderCount}/div; }✅ 规范整改保持 Render 纯净副作用后移至 Commit所有的全局变量修改、DOM 操作、网络上报和定时器必须严格收敛在useEffect或事件处理函数中。Render 阶段必须是一个纯函数Given same props state, return same JSX。陷阱 3Strict Mode 双重挂载下的订阅泄漏在 React 18 开发环境下React.StrictMode会故意模拟一次“挂载 - 卸载 - 重新挂载”的完整生命周期专门帮助开发者揪出未正确清理的副作用。如果在封装自定义事件订阅如 WebSocket、Window Resize、第三方 EventBus时遗漏了 Cleanup开发环境下会立即发现回调被执行了两次// ✅ 标准的事件订阅与对称清理范式 function useWindowSize() { const [size, setSize] useState({ width: window.innerWidth, height: window.innerHeight }); useEffect(() { const handleResize () { setSize({ width: window.innerWidth, height: window.innerHeight }); }; window.addEventListener(resize, handleResize); // 必须返回对称的清理函数 return () { window.removeEventListener(resize, handleResize); }; }, []); return size; }并发副作用治理 Checklist依赖项数组Deps Array诚实声明严禁为了“只想让 Effect 跑一次”而故意留空依赖数组。如果 Effect 内部读取了外部变量要么放入 Deps要么使用useRef保存最新引用要么将函数提取为组件外部纯函数。异步回调内部增加活跃状态守卫Active Flag如果使用的三方 SDK 不支持AbortController使用闭包布尔标记useEffect(() { let isActive true; legacyApi.fetchData().then((res) { if (isActive) setData(res); }); return () { isActive false; }; }, [query]);拥抱 TanStack Query / SWR 等专业数据层在现代 React 架构中尽可能将“数据获取与缓存”交由成熟的数据层库管理它们天然内置了请求去重、取消、竞态锁与垃圾回收机制避免在每个组件中重复手写脆弱的useEffect。