useState 到底怎么工作的?
理解状态快照、更新队列和函数式更新
useState 不是把状态保存在函数变量里,而是让 React 在组件对应的 Fiber 上保存状态,并在下一次渲染时把最新状态传回组件。
返回值包含两部分:当前这次渲染的状态快照,以及用于提交更新的 setter 函数。
1. 状态保存在哪里?
函数组件每次更新时都会重新执行。下面的局部变量不会跨渲染自动保留:
React 会为组件维护 Fiber,并在 Fiber 的 memoizedState 链表中保存 Hook 信息。可以用下面的简化模型理解:
每次渲染调用 useState 时,React 按 Hook 的调用顺序读取对应节点。因此 Hook 不能放在条件、循环或嵌套函数中,否则同一个位置在不同渲染中可能对应不同 Hook。
这里的“对应”不是在两棵 Fiber 树做 diff 时靠变量名或 Hook 类型匹配出来的,而是在函数组件重新执行时建立的。更新开始后,React 会基于当前已提交的 Fiber 创建 work-in-progress Fiber;执行组件函数时,每遇到一次 Hook 调用,就用 Hook 游标从当前 Fiber 的旧 Hook 链表中取出第 N 个节点,并在 work-in-progress Fiber 上生成对应的新 Hook 节点。
因此 Hook 的匹配关系可以理解为:
如果某个 Hook 被写进条件分支,导致这次渲染少调用或多调用了一个 Hook,后续 Hook 的序号就会整体偏移。错位发生在执行函数组件、读取 Hook 链表的阶段,而不是普通子节点 reconciliation 比较阶段。
2. 首次渲染和后续渲染
大致流程如下:
- 首次渲染:React 创建 Hook 节点,保存初始状态
0,并返回[0, setCount]。 - 调用 setter:React 创建一个更新对象,放入该 Hook 的更新队列,并安排组件更新。
- 再次渲染:组件重新执行,React 按相同顺序找到这个 Hook,处理队列中的更新。
- 得到新快照:React 返回新的
[count, setCount],组件使用新值计算 UI。
setCount 不会直接修改当前已经执行完的函数中的 count。它请求 React 在后续渲染中计算一个新的状态。
3. 状态是“快照”,不是普通变量
事件处理函数捕获的是创建它的那次渲染中的变量。调用 setter 后,当前渲染不会被“就地修改”;React 会在下一次渲染时提供新的状态快照。
这也是下面两次更新最终只增加 1 的原因:
如果当前 count 是 0,这两个更新都相当于提交 1,而不是依次读取 0、1。
4. 更新队列与函数式更新
当新状态依赖旧状态时,使用函数式更新:
React 会在处理更新队列时依次执行两个 updater:
因此最终增加 2。
直接传值和函数式更新在构造 work-in-progress Fiber 时没有本质差异:它们都会创建 update,并挂到同一个 Hook 的 queue 上。差异在 update 的 action:
下一次渲染时,React 通过 Hook 游标在 work-in-progress Fiber 上生成对应 Hook,并处理这个 Hook 的 queue。可以把计算逻辑简化理解为:
所以连续两次 setCount(count + 1) 会得到两个“替换为 1”的 update,最终仍然是 1;连续两次 setCount((value) => value + 1) 会基于队列中上一个 update 的计算结果继续加,最终是 2。
函数式更新尤其适合:
- 连续多次更新同一个状态;
- 定时器、Promise 或订阅回调中的状态更新;
- 更新逻辑依赖上一次状态的场景。
如果新值与旧值无关,也可以直接传值:
5. 批量更新不等于立即修改状态
React 可能把同一事件或同一批任务中的多个更新合并处理,以减少重新渲染次数。React 18 还扩展了自动批处理的范围,具体内容见批量更新。
即使这些更新最终只触发一次重新渲染,也不能因此认为 setter 会同步修改当前变量。需要在更新后读取最新值时,应通过下一次渲染、effect 或事件流程获取,而不是紧接着读取当前闭包中的变量。
6. 对象和数组必须使用不可变更新
React 通常使用 Object.is 比较新旧状态。如果新旧值是同一个引用,React 可能认为状态没有变化:
数组也应返回新引用:
注意,不是所有 setter 调用都会导致组件重新渲染;如果新状态与旧状态相同,React 可以跳过这次更新。
7. 初始值和惰性初始化
如果初始状态的计算成本较高,可以传入初始化函数:
初始化函数用于计算初始状态,而不是每次渲染都执行。不要把带副作用的操作放进初始化函数;在开发环境 Strict Mode 下,React 可能额外调用初始化逻辑来发现不纯代码,因此初始化函数应保持纯粹。
对于简单值,直接传值即可:
8. useState 与 useReducer 如何选择?
两者都用于保存状态,区别主要在更新逻辑的组织方式:
useState:状态结构简单,更新逻辑少,直接调用 setter 更直观。useReducer:状态更新类型较多,或者多个字段会一起变化时,用 action 和 reducer 集中管理逻辑更清晰。
无论使用哪一种,都应遵守不可变更新,并使用函数式更新处理依赖旧状态的计算。
常见误区
setter 会立即改变 state
不会。setter 会提交更新,新的 state 要到后续渲染中才会成为当前快照。
连续调用两次 setter 一定渲染两次
不一定。React 可能进行批量更新,具体行为还与 React 版本和更新场景有关。
useState 会深度比较对象
不会。状态比较不是深度比较。对象和数组应通过创建新引用表达变化。
初始值每次渲染都会重新作为 state 使用
不会。初始值只用于初始化对应 Hook;后续渲染会继续使用已有状态。若计算初始值成本高,使用惰性初始化函数。
面试总结
可以这样回答:
useState的状态并不保存在函数局部变量中,而是由 React 保存在组件 Fiber 对应的 Hook 数据中。调用 setter 时,React 会把更新放入队列并安排重新渲染;下一次渲染时,React 按 Hook 调用顺序处理队列,生成新的状态快照。连续更新旧状态时应使用函数式更新,对象和数组则应通过返回新引用来更新。
记住四个关键词:Fiber、Hook 顺序、更新队列、状态快照。

