
去年我们上线了一个实时监控大屏用ReactWebSocket实现数据动态更新。上线首日就爆出严重bug当用户切换筛选条件时图表数据居然错乱显示成上一次的条件结果这个幽灵般的数据滞后问题你们是不是也遇到过现象为什么我的state过期了问题复现场景当快速切换城市筛选器时WebSocket返回的最新数据却被旧的筛选条件处理。关键代码如下function Dashboard() { const [city, setCity] useState(beijing); useEffect(() { const ws new WebSocket(API_URL); ws.onmessage (e) { // ❌ 这里永远拿到的是初始的city值 processData(e.data, city); }; return () ws.close(); }, []); // 空依赖数组 return CitySelector onChange{setCity} /; }你可能会问明明用setCity更新了状态为什么WebSocket回调里还是旧值这里就踩中了React闭包陷阱的经典案例。根因闭包冻住了你的依赖关键在于useEffect的依赖数组为空时其回调会捕获初始化时的闭包。无论后续state如何更新ws.onmessage引用的始终是mount时的city值。这不是React的bug而是JavaScript闭包的工作机制组件首次渲染时city初始值为beijinguseEffect回调执行创建闭包捕获此时的city用户选择shanghai触发setCity组件重新渲染但useEffect没有重新执行ws.onmessage仍然引用旧的闭包解法动态引用最新值的三种姿势方案1添加依赖项触发re-runuseEffect(() { const ws new WebSocket(API_URL); ws.onmessage (e) processData(e.data, city); return () ws.close(); }, [city]); // ✅ city变化时重建WebSocket代价频繁切换会导致WebSocket反复连接/断开方案2用ref保存动态值function Dashboard() { const [city, setCity] useState(beijing); const latestCity useRef(city); useEffect(() { latestCity.current city; // 每次渲染更新ref }, [city]); useEffect(() { const ws new WebSocket(API_URL); ws.onmessage (e) { processData(e.data, latestCity.current); // ✅ 永远拿到最新值 }; return () ws.close(); }, []); }适用场景需要保持长连接且频繁更新依赖项方案3函数式更新仅适用于setStatews.onmessage (e) { setData(prev processData(e.data, prev)); // ✅ 用函数式更新规避闭包 };性能对比re-render vs ref我们在测试环境模拟了高频更新场景每秒10次state变化方案内存占用平均响应延时WebSocket重建次数依赖项触发re-run较低120ms300useRef方案较高15ms1避坑指南useEffect的死亡陷阱空依赖数组陷阱任何依赖state/props却不声明依赖的行为都可能导致闭包问题清理函数滞后在异步操作中直接引用state可能导致清理时访问到过期值useEffect(() { const timer setTimeout(() saveData(id), 1000); return () clearTimeout(timer); // 可能清除的是新timer而旧回调仍在执行 }, [id]);对象依赖项浅比较当依赖项是对象/数组时容易因引用变化导致非预期re-run多状态不一致当多个state存在逻辑关联时分开更新可能导致中间状态不一致核心结论对于任何effect中使用的依赖项只有两种正确选择要么声明依赖触发re-run要么用ref主动管理最新引用。没有中间态。你在处理复杂effect逻辑时有什么独门技巧欢迎分享你的闭包战争故事。