【图】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 完全一致,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 的源码同源关系

这张图揭示 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 普通值)

这张图深入 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
相关推荐
随性而行3606 分钟前
企业微信二次开发如何接入大模型工具?API接口实现智能任务调用的技术思路
java·前端·人工智能·python·微信·机器人·企业微信
IMPYLH15 分钟前
HTML 的 <td> 元素
前端·html
gnip26 分钟前
Flutter GetX 三件套开发规范(Skill)
前端·flutter
张小姐的猫36 分钟前
【AI大模型接入SDK】 —— 前端页面 & 项目总结与拓展
前端·数据结构·数据库·c++·人工智能·chatgpt
web打印社区38 分钟前
远程打印:WebSocket 与 HTTP 轮询怎么选
前端·vue.js·websocket·网络协议·http·electron·pdf
吴声子夜歌42 分钟前
HTML——庞杂的表单控件元素(一)
前端·html
天天喝旺仔1 小时前
浏览器渲染原理:从解析 HTML/CSS、构建渲染树到重排重绘与首屏优化
前端·javascript·css·性能优化·html
明月_清风1 小时前
前端已死?别急,这可能只是所有行业的开始
前端·ai编程
明月_清风1 小时前
干了 6 年前端,我是怎么一步步转型到 AI 的?
前端·后端·ai编程
乘风gg1 小时前
花了 100 亿 Token 后,我发现 Code is cheap 是最大的谎言
前端·ai编程·claude