去年我们上线了一个实时监控大屏,用React+WebSocket实现数据动态更新。上线首日就爆出严重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初始值为'beijing'
- useEffect回调执行,创建闭包捕获此时的city
- 用户选择'shanghai'触发setCity,组件重新渲染
- 但useEffect没有重新执行,ws.onmessage仍然引用旧的闭包
解法:动态引用最新值的三种姿势
方案1:添加依赖项触发re-run
useEffect(() => { 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:函数式更新(仅适用于setState)
ws.onmessage = (e) => { setData(prev => processData(e.data, prev)); // ✅ 用函数式更新规避闭包 };性能对比:re-render vs ref
我们在测试环境模拟了高频更新场景(每秒10次state变化):
| 方案 | 内存占用 | 平均响应延时 | WebSocket重建次数 |
|---|---|---|---|
| 依赖项触发re-run | 较低 | 120ms | 300+ |
| useRef方案 | 较高 | 15ms | 1 |
避坑指南:useEffect的死亡陷阱
useEffect(() => { const timer = setTimeout(() => saveData(id), 1000); return () => clearTimeout(timer); // 可能清除的是新timer而旧回调仍在执行 }, [id]);核心结论
- 对于任何effect中使用的依赖项,只有两种正确选择:要么声明依赖触发re-run,要么用ref主动管理最新引用。没有中间态。
你在处理复杂effect逻辑时有什么独门技巧?欢迎分享你的闭包战争故事。