useReducer 的挂载(Mount)与更新(Update)完整流程
这张图展示 useReducer 在首次挂载和后续更新时,Fiber 节点上 Hook 对象的创建与状态计算逻辑,并特别标注了 惰性初始化(Lazy Initialization) 的分支。
📝 源码级深度解析(数据结构与初始化) :
- 惰性初始化(Lazy Initialization) :
useReducer支持传入第三个参数init函数。如果提供了init,React 在首次挂载时会调用init(initialArg)来计算初始状态,而不是直接使用initialArg。这适用于需要复杂计算才能得到初始状态 的场景(如从localStorage读取解析),并且计算逻辑只在首次渲染时执行一次,避免了每次渲染都重复计算。 - 状态存储 :与
useState完全一致,useReducer的状态值存储在hook.memoizedState中,更新队列存储在hook.queue中。 dispatch函数的稳定性 :在mountReducer中创建的dispatch函数通过闭包绑定了当前 Fiber 节点和queue对象。即使在后续更新中,这个dispatch函数的内存地址永远不会改变 ,因此可以安全地作为props传递给子组件,或放入useEffect的依赖数组中。updateReducer是核心枢纽 :无论是useState还是useReducer,只要组件进入更新阶段,状态的计算逻辑最终都汇集到updateReducer。这是 React 源码中状态管理的"大脑"。
dispatch 调度函数的工作流程与 Action 入队
这张图展示调用 dispatch(action) 时,Action 对象如何入队,并触发调度器安排更新任务。
📝 源码级深度解析(调度与派发) :
- 渲染期间禁止派发更新 :React 源码中明确检查
renderPhaseUpdates,如果检测到在 Render 阶段调用dispatch,会立即抛出错误(Cannot update during an existing state transition)。这是因为 Render 阶段是纯计算过程,不允许副作用改变状态导致无限循环。 - 批处理(Batching)机制 :与
useState完全一致,useReducer的dispatch同样遵循executionContext的批处理规则。在 React 18 的createRoot下,即使在setTimeout或Promise中多次调用dispatch,也会自动合并为一次渲染。 - 环状链表入队的 O(1) 性能 :
queue.pending指向最后一个 Update,入队操作为pending.next = update,时间复杂度恒为 O(1),无论队列多长。 - 优先级 Lane 的传递 :每个
Update对象携带一个lane位(优先级)。调度器会根据最高优先级的lane决定任务执行顺序,这确保了紧急更新(如用户输入)能优先于后台数据更新。
useReducer 与 useState 的源码同源关系
这张图揭示 useState 实际上是 useReducer 的"语法糖",两者在挂载和更新阶段的源码路径高度重合。
📝 源码级深度解析(同源关系) :
-
mountStatevsmountReducer:mountState内部会调用mountWorkInProgressHook创建 Hook 节点,并额外初始化一个basicStateReducer。这个处理器逻辑很简单:如果action是函数,则执行action(prevState);否则直接返回action。mountReducer则是将开发者传入的自定义 reducer 直接挂载到 Hook 节点上,供后续processUpdateQueue调用。
-
updateState完全等于updateReducer:- 在 React 18 源码(
ReactFiberHooks.js)中,function updateState(initialState) { return updateReducer(basicStateReducer, initialState); }。 - 这意味着只要组件完成首次挂载,后续
useState的执行路径与useReducer完全一致 ,唯一的区别是默认使用的reducer规则不同。
- 在 React 18 源码(
-
为什么
useState不传init参数 :因为useState的场景是简单的值或对象,不需要复杂的惰性初始化。如果需要复杂初始化,useReducer提供init参数更合适。 -
性能考量 :由于
useState是useReducer的特化版本,它在源码级别没有额外的性能开销。两者在更新阶段的比较逻辑完全相同
processUpdateQueue 中的 Action 处理流程(Reducer vs 普通值)
这张图深入 processUpdateQueue 内部,展示 useReducer 如何处理不同的 action 类型,以及它如何与 useState 共享计算逻辑。
📝 源码级深度解析(状态计算) :
-
useState的basicStateReducer逻辑:- 如果
action是函数:return action(prevState)(如(prev) => prev + 1)。 - 如果
action是普通值:return action(如setState(5)直接覆盖)。
- 如果
-
useReducer的自定义 Reducer:- 始终调用
reducer(state, action),返回值为新状态。 - 标准的 Redux 风格的
(state, action) => newState。
- 始终调用
-
相等性检查(
Object.is) :在processUpdateQueue计算完新状态后,React 会使用Object.is比较新旧状态值。如果完全相同 ,React 会跳过该 Fiber 节点的子节点协调(bailout优化),从而提升性能。但注意:调度已经发生,即使值相同,组件的 Render 阶段依然会执行,只是子节点会被跳过。 -
useReducer与useState的执行效率 :由于useState的basicStateReducer逻辑极其简单,其计算开销远低于自定义复杂 Reducer。但两者在遍历更新队列(processUpdateQueue)上的耗时完全一致,取决于队列长度。 -
并发模式下的处理 :
processUpdateQueue会依据Update对象的lane(优先级)进行过滤,只计算当前渲染批次允许的lane范围内的更新,剩余的更新保留在队列中等待后续批次处理。这是 Concurrent Mode 实现"部分渲染"的基石。
📊 四张图总结:useReducer 的完整画像
| 维度 | 核心结论 |
|---|---|
| 数据结构与初始化 | useReducer 支持惰性初始化(init 函数),状态存储在 hook.memoizedState 中。 |
| 调度与派发 | dispatch 通过闭包绑定 Fiber 节点,入队后依据 executionContext 决定批处理或立即执行。 |
| 同源关系 | useState 是 useReducer 的语法糖,updateState 直接指向 updateReducer 源码。 |
| 状态计算流程 | processUpdateQueue 区别处理函数式更新与普通值,并通过 Object.is 决定是否 bailout |