查看: 1558|回复: 3

React Hooks单向链表结构与挂载更新阶段源码解析

[复制链接]
发表于 昨天 05:00 | 显示全部楼层 |阅读模式
很多前端开发者都背过“只能在函数组件顶层调用 Hook”这条规则,eslint-plugin-react-hooks 也会把循环、条件分支里的 Hook 调用标红。但这条限制并不是框架层面的口头约定,而是由 React 内部保存 Hook 的数据结构决定的:函数组件对应的 Fiber 节点上,所有 Hook 会被串成一条单向链表,链表头挂在 fiber.memoizedState 上,React 重新渲染时只能按调用顺序逐个还原状态。

一、Hook 节点与 fiber.memoizedState 链表

在 React 源码中,每个函数组件对应一个 Fiber 节点。组件内按顺序声明的 useState、useEffect、useMemo 等 Hook,在底层都会被实例化为 Hook 对象,并通过 next 指针串联。原文给出的 Hook 结构如下:
  1. export interface Hook {
  2.   memoizedState: any; // 保存当前 Hook 的状态(useState 的值、useEffect 的销毁函数与依赖项等)
  3.   baseState: any; // 初始基准状态
  4.   baseQueue: Update<any> | null; // 待处理的更新队列
  5.   queue: UpdateQueue<any> | null; // 本轮产生的更新环状链表
  6.   next: Hook | null; // 指向组件中声明的下一个 Hook
  7. }
复制代码

这条链表的走向可以概括为:Fiber 节点的 memoizedState 指向第一个 Hook,第一个 Hook 的 next 指向第二个 Hook,第二个 Hook 的 next 指向第三个 Hook,最后一个 Hook 的 next 为 null。例如组件里依次写了 useState(count)、useEffect(log)、useMemo(heavyCalc),它们就会按这个顺序成为链表上的三个节点。

关键点在于,Hook 对象本身没有名字,也没有唯一的 Key 标识。React 在组件更新时无法通过“名称”找到某个 Hook,只能依靠链表的遍历顺序来还原每个 Hook 对应的历史状态。这也解释了为什么条件分支会引发问题:如果某次渲染用 if (condition) 跳过了中间的 Hook 2,那么原本属于 Hook 3 的 useMemo 就会错误地读取 Hook 2 的旧状态,导致整条状态链表错位,严重时直接崩溃。

二、挂载与更新使用两套 Dispatcher

React 针对组件生命周期的不同阶段,准备了两套不同的 Dispatcher 实现字典:HooksDispatcherOnMount 和 HooksDispatcherOnUpdate。首次挂载时走挂载逻辑,组件因 setState 重新执行时切换到更新逻辑,两者操作的是同一条链表的不同阶段。

1. 首次挂载阶段

组件第一次执行时,每次调用 useState 或 useEffect,都会进入 mountWorkInProgressHook()。它的职责很明确:实例化一个新的 Hook 对象,用尾插法追加到当前 Fiber 的 memoizedState 链表末尾,并推进 workInProgressHook 这个全局游标指针。
  1. function mountWorkInProgressHook() {
  2.   const hook = {
  3.     memoizedState: null,
  4.     baseState: null,
  5.     baseQueue: null,
  6.     queue: null,
  7.     next: null,
  8.   };
  9.   if (workInProgressHook === null) {
  10.     // 链表的首个节点
  11.     currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  12.   } else {
  13.     // 追加到已有链表末尾
  14.     workInProgressHook = workInProgressHook.next = hook;
  15.   }
  16.   return workInProgressHook;
  17. }
复制代码

从代码可以看到,第一个 Hook 会同时成为 Fiber 的 memoizedState 和当前游标;后续 Hook 则通过 workInProgressHook.next = hook 接到尾部。整个挂载过程不依赖任何 Hook 名称,只依赖调用次序。

2. 更新阶段

当组件因为 setState 重新执行时,Dispatcher 切换为 HooksDispatcherOnUpdate,此时调用的是 updateWorkInProgressHook()。它要完成的事情是:从已有的 currentFiber.memoizedState(上一次渲染完成的 Fiber 链表)中按顺序取出旧 Hook 节点,克隆或复用该 Hook,读取其保存的 memoizedState 和未决的更新队列 queue,再处理所有积压的更新 Action,算出新状态并返回。
  1. function updateWorkInProgressHook() {
  2.   let nextCurrentHook;
  3.   if (currentHook === null) {
  4.     const current = currentlyRenderingFiber.alternate;
  5.     if (current !== null) {
  6.       nextCurrentHook = current.memoizedState; // 从旧 Fiber 根部读取
  7.     } else {
  8.       nextCurrentHook = null;
  9.     }
  10.   } else {
  11.     nextCurrentHook = currentHook.next; // 顺着链表后移一位
  12.   }
  13.   currentHook = nextCurrentHook;
  14.   // 创建对应的 WorkInProgress Hook 副本并按序推进
  15.   const newHook = {
  16.     memoizedState: currentHook.memoizedState,
  17.     baseState: currentHook.baseState,
  18.     baseQueue: currentHook.baseQueue,
  19.     queue: currentHook.queue,
  20.     next: null,
  21.   };
  22.   if (workInProgressHook === null) {
  23.     currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
  24.   } else {
  25.     workInProgressHook = workInProgressHook.next = newHook;
  26.   }
  27.   return workInProgressHook;
  28. }
复制代码

这里最值得注意的是 currentHook 的推进方式:第一次从 currentlyRenderingFiber.alternate 的 memoizedState 取头节点,之后每次取 currentHook.next,相当于沿着旧链表一位一位向后走。新 Hook 会把旧 Hook 的 memoizedState、baseState、baseQueue、queue 复制过来,next 先置空,再按同样的顺序挂到新的 workInProgress 链表上。因此,新旧两条链表必须保持完全相同的节点顺序,否则复制状态时就会发生“张冠李戴”。

三、useEffect 的环形 Effect 链表

除了保存状态的 Hook,useEffect 产生的副作用还会被打包成一个独立的环形链表,挂载在 fiber.updateQueue 上。它和 memoizedState 上的 Hook 链表是两套结构,但同样依赖声明顺序和调度阶段。

在 Commit 阶段,React 的处理时序可以拆成三部分:Before Mutation / Mutation 阶段处理 DOM 节点的增删改;Layout 阶段同步执行 useLayoutEffect 的销毁与挂载回调;Passive Effects 阶段则通过 Scheduler 异步调度,执行 useEffect 的清理函数并触发新的 effect 回调,从而不阻塞浏览器的首屏绘制。理解这个时序,有助于排查“为什么 useEffect 在绘制后才执行”“为什么清理函数先于下一次 effect 执行”这类问题。

四、从链表结构反推开发约束

看清 mountWorkInProgressHook 的尾插和 updateWorkInProgressHook 的顺序取值,就能明白 Hook 规则的本质:状态不是按变量名存储的,而是按调用槽位存储的。条件调用、循环调用或嵌套函数中调用,都会改变某次渲染的槽位数量或顺序,让更新阶段读错节点。自定义 Hook 同样如此,它只是把多个 Hook 调用封装起来,内部依然要保证每次渲染顺序稳定。

实际排查时,如果遇到状态串位、useEffect 依赖错乱或组件更新后行为异常,可以先检查是否存在提前 return、条件分支包裹 Hook、动态循环生成 Hook 等写法,再借助 eslint-plugin-react-hooks 定位。规则不是形式主义,而是单向链表结构对调用顺序提出的硬性要求。
回复

使用道具 举报

发表于 昨天 05:05 | 显示全部楼层

Re: React Hooks单向链表结构与挂载更新阶段源码解析

这个解析把“只能在顶层调用 Hook”背后的数据结构讲得很直观,之前只是记规则,现在能对应到 fiber.memoizedState 上的单向链表了。特别是 mountWorkInProgressHook 里尾插加 workInProgressHook 游标推进那段,能看出挂载阶段完全不依赖 Hook 名字,只依赖调用顺序;更新阶段又从 current.alternate 的 memoizedState 出发沿 next 逐个取旧 Hook,条件分支一跳过,后面自然就整体错位,这个因果关系说得挺清楚。 不过首帖末尾好像贴到 const newHook = { ... } 那里就截断了,后面 newHook.next 怎么接到新链表、以及 queue 里的 pending 更新怎么处理没显示出来。如果方便的话可以补一下更新阶段剩下的部分,尤其是 baseQueue 和 queue 的区别,还有 renderWithHooks 里 Dispatcher 从 mount 切到 update 的时机,感觉这块和“Hooks 顺序稳定”这条规则的关联会更完整。
回复 支持 反对

使用道具 举报

发表于 昨天 05:05 | 显示全部楼层

Re: React Hooks单向链表结构与挂载更新阶段源码解析

这篇梳理得挺清楚,尤其是把 fiber.memoizedState 作为链表头、Hook 通过 next 串联、以及挂载和更新分别走两套 Dispatcher 这条线讲明白了,对理解“为什么只能在顶层调用 Hook”很有帮助。用 workInProgressHook 游标按顺序推进来解释状态还原,也比单纯背规则更容易记住。 不过首帖里 updateWorkInProgressHook 的代码最后似乎被截断了,停在创建 newHook 之后。如果方便的话,可以补全从 alternate 读取旧链表、克隆或复用 Hook、处理 queue 中积压更新并计算新状态这一段,再顺带把更新阶段游标如何推进讲完整,整篇会更闭环。整体内容很扎实,期待后续。
回复 支持 反对

使用道具 举报

发表于 昨天 05:05 | 显示全部楼层

Re: React Hooks单向链表结构与挂载更新阶段源码解析

这篇把“只能在顶层调用 Hook”从规则层面落到 fiber.memoizedState 单向链表上,讲得很直观,尤其是 Hook 没有名称和 Key、更新时只能按调用顺序逐个还原这一点,基本就把条件分支导致状态错位的原因说透了。挂载阶段 mountWorkInProgressHook 的尾插法,以及挂载和更新分别走两套 Dispatcher 的划分也很清楚,workInProgressHook 作为游标推进链表的思路很好理解。 不过更新阶段那段 updateWorkInProgressHook 的代码好像贴到 newHook 的 next 就截断了,后面按序推进游标、把新 Hook 接到 workInProgress 链表上以及返回新 Hook 的逻辑没展开,读到这里会有点断档。如果能补上 currentHook 和 workInProgressHook 在每次渲染前的重置、alternate 的读取条件,以及 update 阶段如何处理 queue 中积压的更新 Action,整条链路会更完整。另外像 useState 的 memoizedState 存的是状态值,而 useEffect 存的是 effect 对象这类差异,也可以在链表结构部分稍微点一下,方便读者把不同 Hook 的节点含义区分开。整体方向很好,适合作为理解 Hooks 底层数据结构的入门梳理。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-31 07:55 , Processed in 0.022079 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部