【图】React源码解析-从数据结构、调度器、源码设计模式到状态计算引擎,深挖useReducer原理

97 阅读5分钟

React源码解析 宏观架构 (12).png

useReducer 的挂载(Mount)与更新(Update)完整流程

deepseek_mermaid_20260716_6809dd.png 这张图展示 useReducer 在首次挂载和后续更新时,Fiber 节点上 Hook 对象的创建与状态计算逻辑,并特别标注了 惰性初始化(Lazy Initialization)  的分支。

  1. 惰性初始化(Lazy Initialization)useReducer 支持传入第三个参数 init 函数。如果提供了 init,React 在首次挂载时会调用 init(initialArg) 来计算初始状态,而不是直接使用 initialArg。这适用于需要复杂计算才能得到初始状态的场景(如从 localStorage 读取解析),并且计算逻辑只在首次渲染时执行一次,避免了每次渲染都重复计算。
  2. 状态存储:与 useState 完全一致,useReducer 的状态值存储在 hook.memoizedState 中,更新队列存储在 hook.queue 中。
  3. dispatch 函数的稳定性:在 mountReducer 中创建的 dispatch 函数通过闭包绑定了当前 Fiber 节点和 queue 对象。即使在后续更新中,这个 dispatch 函数的内存地址永远不会改变,因此可以安全地作为 props 传递给子组件,或放入 useEffect 的依赖数组中。
  4. updateReducer 是核心枢纽:无论是 useState 还是 useReducer,只要组件进入更新阶段,状态的计算逻辑最终都汇集到 updateReducer。这是 React 源码中状态管理的“大脑”。

dispatch 调度函数的工作流程与 Action 入队

deepseek_mermaid_20260716_f6d7c7.png 这张图展示调用 dispatch(action) 时,Action 对象如何入队,并触发调度器安排更新任务。

  1. 渲染期间禁止派发更新:React 源码中明确检查 renderPhaseUpdates,如果检测到在 Render 阶段调用 dispatch,会立即抛出错误(Cannot update during an existing state transition)。这是因为 Render 阶段是纯计算过程,不允许副作用改变状态导致无限循环。
  2. 批处理(Batching)机制:与 useState 完全一致,useReducer 的 dispatch 同样遵循 executionContext 的批处理规则。在 React 18 的 createRoot 下,即使在 setTimeout 或 Promise 中多次调用 dispatch,也会自动合并为一次渲染。
  3. 环状链表入队的 O(1) 性能queue.pending 指向最后一个 Update,入队操作为 pending.next = update,时间复杂度恒为 O(1),无论队列多长。
  4. 优先级 Lane 的传递:每个 Update 对象携带一个 lane 位(优先级)。调度器会根据最高优先级的 lane 决定任务执行顺序,这确保了紧急更新(如用户输入)能优先于后台数据更新。

useReducer 与 useState 的源码同源关系

deepseek_mermaid_20260716_25d52e.png 这张图揭示 useState 实际上是 useReducer 的“语法糖”,两者在挂载和更新阶段的源码路径高度重合。

  1. mountState vs mountReducer

    • mountState 内部会调用 mountWorkInProgressHook 创建 Hook 节点,并额外初始化一个 basicStateReducer。这个处理器逻辑很简单:如果 action 是函数,则执行 action(prevState);否则直接返回 action
    • mountReducer 则是将开发者传入的自定义 reducer 直接挂载到 Hook 节点上,供后续 processUpdateQueue 调用。
  2. updateState 完全等于 updateReducer

    • 在 React 18 源码(ReactFiberHooks.js)中,function updateState(initialState) { return updateReducer(basicStateReducer, initialState); }
    • 这意味着只要组件完成首次挂载,后续 useState 的执行路径与 useReducer 完全一致,唯一的区别是默认使用的 reducer 规则不同。
  3. 为什么 useState 不传 init 参数:因为 useState 的场景是简单的值或对象,不需要复杂的惰性初始化。如果需要复杂初始化,useReducer 提供 init 参数更合适。

  4. 性能考量:由于 useState 是 useReducer 的特化版本,它在源码级别没有额外的性能开销。两者在更新阶段的比较逻辑完全相同

processUpdateQueue 中的 Action 处理流程(Reducer vs 普通值)

deepseek_mermaid_20260716_b53757.png 这张图深入 processUpdateQueue 内部,展示 useReducer 如何处理不同的 action 类型,以及它如何与 useState 共享计算逻辑。

  1. useState 的 basicStateReducer 逻辑

    • 如果 action 是函数:return action(prevState)(如 (prev) => prev + 1)。
    • 如果 action 是普通值:return action(如 setState(5) 直接覆盖)。
  2. useReducer 的自定义 Reducer

    • 始终调用 reducer(state, action),返回值为新状态。
    • 标准的 Redux 风格的 (state, action) => newState
  3. 相等性检查(Object.is :在 processUpdateQueue 计算完新状态后,React 会使用 Object.is 比较新旧状态值。如果完全相同,React 会跳过该 Fiber 节点的子节点协调(bailout 优化),从而提升性能。但注意:调度已经发生,即使值相同,组件的 Render 阶段依然会执行,只是子节点会被跳过。

  4. useReducer 与 useState 的执行效率:由于 useState 的 basicStateReducer 逻辑极其简单,其计算开销远低于自定义复杂 Reducer。但两者在遍历更新队列(processUpdateQueue)上的耗时完全一致,取决于队列长度。

  5. 并发模式下的处理processUpdateQueue 会依据 Update 对象的 lane(优先级)进行过滤,只计算当前渲染批次允许的 lane 范围内的更新,剩余的更新保留在队列中等待后续批次处理。这是 Concurrent Mode 实现“部分渲染”的基石。


useReducer 的完整画像

维度核心结论
数据结构与初始化useReducer 支持惰性初始化(init 函数),状态存储在 hook.memoizedState 中。
调度与派发dispatch 通过闭包绑定 Fiber 节点,入队后依据 executionContext 决定批处理或立即执行。
同源关系useState 是 useReducer 的语法糖,updateState 直接指向 updateReducer 源码。
状态计算流程processUpdateQueue 区别处理函数式更新与普通值,并通过 Object.is 决定是否 bailout