内容提要
文章基于 React 16.11 源码讲解 useState:先介绍 Hooks 的动机与 API,再说明 useState 的用法及“顶层调用、顺序一致”规则,并通过三个 Demo 逐步实现带记忆的 useState,解释该规则源于按顺序存储状态。最后解析源码:首次渲染调用 mountState,更新时走 updateState/updateReducer,dispatchAction 负责记录更新并调度重渲染。
延伸解读
从“面向生命周期”到“面向业务逻辑”
文章指出,Hooks 的动机之一是解决复杂组件难以理解的问题:Class Component 常把相关逻辑分散在 componentDidMount、componentDidUpdate 等生命周期中,导致代码臃肿。Hooks 让开发者不再围绕生命周期组织代码,而是按业务逻辑聚合。这一转变不仅影响写法,也影响组件可读性与维护成本,尤其适合状态逻辑复杂、生命周期交叉较多的组件。
“顶层调用”规则源于按顺序存储
文章通过三个 Demo 逐步说明:useState 的记忆能力依赖一个数组按调用顺序存放状态,并用索引对应每次调用。若在条件、循环或嵌套函数中调用 Hook,顺序可能变化,而存储结构不会感知,导致状态错位。因此规则并非风格约束,而是实现机制的直接结果。理解这一点后,开发者更容易判断哪些写法会破坏 Hook 的稳定性。
useState 本质上是 useReducer 的特例
源码解析显示,首次渲染走 mountState,更新时 updateState 直接调用 updateReducer,并传入 basicStateReducer。该 reducer 对普通值直接返回 action,对函数则调用后返回结果。这说明 useState 并非独立复杂机制,而是复用更新队列与 reducer 流程的简化入口。读者可借此理解 useState 与 useReducer 在源码层面的关系,以及为什么更新逻辑能统一处理。
更新队列与 re-render 的边界
dispatchAction 会把更新记录进队列并调度渲染;若在渲染过程中再次触发更新,则进入 re-render 分支,更新被暂存到 renderPhaseUpdates,并由 numberOfReRenders 计数。文章提到,若把 setState 直接写在函数顶部可能导致无限 re-render,React 会以 RE_RENDER_LIMIT 报错。这提示读者:理解更新队列有助于定位重复渲染问题,但 Fiber 调度细节文章并未展开。
Q&A
为什么 React Hooks 要求必须在顶层调用,不能放在条件或循环里?
因为 React 内部使用一个单向链表(或数组)按 Hooks 的调用顺序存储状态,每个 Hook 通过 next 指针串联。如果 Hooks 的调用顺序发生变化,React 无法正确匹配到对应的状态,导致状态错乱。因此必须确保每次渲染时 Hooks 的调用顺序完全一致,所以只能在最顶层调用。
useState 在首次渲染和更新时分别调用了哪些内部函数?
首次渲染时,useState 实际调用 mountState,创建新的 Hook 并初始化状态和更新队列。更新时,useState 调用 updateState,而 updateState 内部又调用 updateReducer 来处理状态更新。
useState 和 useReducer 有什么关系?
useState 本质上是 useReducer 的一种特殊情况。updateState 内部调用 updateReducer,并传入一个内置的 basicStateReducer,该 reducer 直接返回 action 的值(如果 action 是函数则调用它)。而 useReducer 允许传入自定义的 reducer 函数。
自己实现 useState 时,如何让组件在多次渲染间记住状态?
可以使用一个外部数组 memoizedState 来按顺序存储每个 useState 的状态,并用一个游标 cursor 来记录当前索引。每次调用 useState 时,根据 cursor 读取或初始化状态,并返回 setState 函数,该函数更新数组中对应索引的值并触发重新渲染。
dispatchAction 在 useState 中起什么作用?
dispatchAction 是 useState 返回的 setState 函数背后的实现。它负责创建更新对象(Update),将更新添加到 Hook 的更新队列(queue)中,并调用 scheduleWork 调度重新渲染。在 re-render 场景下,它还会将更新记录到 renderPhaseUpdates 中。
React Hooks 相比 Class Component 有哪些优势?
Hooks 允许在函数组件中使用状态等特性,避免了 class 的额外开销(如创建实例、绑定事件处理器),代码更简洁。它解决了高阶组件嵌套地狱的问题,使状态逻辑复用更容易,并且可以将 UI 与状态分离,让代码更函数式、更新粒度更细。