
前言:useState 真的只是存个值吗?
很多初学者(包括我)在刚接触 React Hooks 时,觉得 useState 简直是开发神器。但随着项目复杂度增加,你总会遇到一些诡异的 Bug:明明写了三行更新代码,页面只变了 1 次?明明状态更新了,定时器里拿到的还是旧值?
今天,咱们就来撕开 useState 的“温情面纱”,看看它底层的“残酷真相”。
场景一:消失的“+1” —— 批量更新与快照逻辑
业务场景
我们要写一个计数器,用户点击按钮时,我们希望逻辑里能连续触发三次加法。
初始代码(直觉误区)
const [count, setCount] = useState<number>(0); |
深度分析:为什么失效了?
这里有两个核心概念:批处理(Batching) 和 渲染快照(Snapshot)。
- 渲染快照:在 React 中,每一次渲染都有它自己的
Props和State。当你调用setCount(count + 1)时,这个count是当前渲染周期里的常量。即便你写了三行,由于这三行代码里的count都是0,React 接收到的指令其实是:“请把下一次渲染的count设为0 + 1”。 - 异步批处理:React 为了性能,会将同一个事件处理函数中的多个
setState合并处理。
解决方案:函数式更新
const handleAdd = () => { |
强烈建议:“如果你需要基于当前状态计算下一个状态,永远优先使用函数式更新。”
场景二:被锁死的时间 —— 闭包陷阱(Stale Closure)
业务场景
我们需要一个定时器,每秒打印当前的 count 值。
踩坑代码
useEffect(() => { |
深度分析:为什么拿不到最新值?
这就是闭包陷阱
useEffect的回调函数在组件挂载时创建,它“捕捉”了当时闭包环境里的count(即 0)。- 由于依赖项数组是
[],这个 Effect 永远不会重新执行。即便外层的count变了,定时器里的函数依然引用着那个出生时的count: 0。
横向对比 (Vue3):在 Vue3 中,
ref本质是一个 Proxy 对象,我们访问的是count.value。即使函数地址没变,但我们始终是通过引用去“盒子”里拿最新的值,所以 Vue 不存在这种闭包陷阱。
解决方案
- 添加依赖项(不推荐):让 Effect 随
count变化而重启(但会导致定时器频繁重置)。 - 使用 Ref(推荐):利用
useRef创造一个不触发渲染但能保持引用的“盒子”。
代码演示
const latestCount = useRef(count); |
场景三:引用类型的“冷漠” —— 不可变数据(Immutability)
业务场景
更新一个用户对象的年龄。
错误演示
const [user, setUser] = useState({ name: "Jack", age: 18 }); |
深度分析:Object.is 的判断
React 底层使用 Object.is 算法来对比新旧状态。
当你执行 user.age = 20 时,user 对象在内存里的地址(指针)完全没变。React 进行判断:“既然引用地址没变,那数据就没变,不需要重渲染。”
解决方案:拥抱不可变性,创造全新对象
const updateAge = () => { |
避坑总结
- 状态不是变量,是快照:每一次渲染都是一张独立的照片。
- 不要在异步回调里迷信 State:如果要在
setTimeout或setInterval里拿最新值,考虑useRef或函数式更新。 - 永远不要修改原对象:在 React 里,
state是只读的。想更新?请先 Copy 副本!