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

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

这张图展示 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 入队

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

📝 源码级深度解析(调度与派发)

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

useReduceruseState 的源码同源关系

这张图揭示 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. 性能考量 :由于 useStateuseReducer 的特化版本,它在源码级别没有额外的性能开销。两者在更新阶段的比较逻辑完全相同

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

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

📝 源码级深度解析(状态计算)

  1. useStatebasicStateReducer 逻辑

    • 如果 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. useReduceruseState 的执行效率 :由于 useStatebasicStateReducer 逻辑极其简单,其计算开销远低于自定义复杂 Reducer。但两者在遍历更新队列(processUpdateQueue)上的耗时完全一致,取决于队列长度。

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


📊 四张图总结:useReducer 的完整画像

维度 核心结论
数据结构与初始化 useReducer 支持惰性初始化(init 函数),状态存储在 hook.memoizedState 中。
调度与派发 dispatch 通过闭包绑定 Fiber 节点,入队后依据 executionContext 决定批处理或立即执行。
同源关系 useStateuseReducer 的语法糖,updateState 直接指向 updateReducer 源码。
状态计算流程 processUpdateQueue 区别处理函数式更新与普通值,并通过 Object.is 决定是否 bailout
相关推荐
willes2 小时前
列表中的倒计时组件
前端
LVZ2 小时前
用了半年 AI 写代码,最烦的不是代码,是环境
前端·后端·开源
polaris_tl3 小时前
一行 `compress: false`,为什么让 SSE 首包恢复实时返回
前端
海边的云3 小时前
别再让 AI 瞎改代码:一套让大模型「收敛」的前端专家 Skill》
前端
kisshyshy3 小时前
给端侧大模型装上“发动机”:React 合成事件 + 进度条组件全解
前端·react.js·node.js
hunterandroid3 小时前
DataStore 工程化实践:迁移、并发更新与异常恢复
android·前端
晓说前端3 小时前
TypeScript 核心语法进阶 —— 字面量类型与类型推论
前端·typescript
不简说3 小时前
JS 代码技巧 vol.8 — 20 个函数式编程实战,把 if/else 拍扁的骚操作
前端·javascript·面试
头茬韭菜3 小时前
4.9 SSRF 防护 — Web 工具的出站安全与私有 IP 拦截
前端·tcp/ip·安全