React Hooks 的底层核心是单向链表结构:每个函数组件对应一个 Fiber 节点,组件内声明的所有 Hook 按调用顺序被实例化为链表节点,通过 next 指针串联,链表头挂在 fiber.memoizedState 上。
React 深度解析:Hooks 链表结构与更新挂载
从 React 16.8 推出 Hooks 以来,“只能在函数顶层调用 Hook,严禁在循环、条件分支或嵌套函数中调用”这条铁律就已经刻在每位前端开发者的心头。ESLint 插件 eslint-plugin-react-hooks 会无情地将违规代码标红。
但为什么 React 必须施加如此苛刻的限制?底层究竟是怎样的物理数据结构在支撑 useState、useEffect 和 useMemo?
答案就在 Fiber 节点上的单向链表结构:fiber.memoizedState。
Hook 节点的单向链表物理结构
在 React 源码中,每个函数组件对应一个 Fiber 节点。组件内部按顺序声明的所有 Hooks,在底层被实例化为一个由 next 指针串联起来的单向链表(LinkedList):
export interface Hook {
memoizedState: any; // 保存当前 Hook 的状态(useState 的值、useEffect 的销毁函数与依赖项等)
baseState: any; // 初始基准状态
baseQueue: Update | null; // 待处理的更新队列
queue: UpdateQueue | null; // 本轮产生的更新环状链表
next: Hook | null; // 指向组件中声明的下一个 Hook
}
graph LR
Fiber[Fiber Node: MyComponent] -->|memoizedState| Hook1["Hook 1 (useState: count)"]
Hook1 -->|next| Hook2["Hook 2 (useEffect: log)"]
Hook2 -->|next| Hook3["Hook 3 (useMemo: heavyCalc)"]
Hook3 -->|next| NullPtr[null]
由于 Hook 对象本身没有任何名字或唯一的 Key 标识符,React 在组件重新渲染(Re-render)时,完全依靠**链表的遍历顺序(Order of Execution)**来精确还原每一个 Hook 对应的历史状态。
如果在某次渲染中,一个 if (condition) 分支跳过了中间的 Hook 2,那么原本属于 Hook 3 的 useMemo 就会错误地读取 Hook 2 的旧状态,导致整个状态链表发生灾难性的错位与崩溃。
挂载阶段(mountWorkInProgressHook)与更新阶段(updateWorkInProgressHook)
React 针对组件生命周期的不同阶段,准备了两套完全不同的 Dispatcher 实现字典:HooksDispatcherOnMount 和 HooksDispatcherOnUpdate。
1. 首次挂载阶段(Mount)
当组件第一次执行时,每次调用 useState 或 useEffect,都会调用 mountWorkInProgressHook():
- 实例化一个新的
Hook对象; - 尾插法追加到当前 Fiber 的
memoizedState链表末尾; - 推进
workInProgressHook全局游标指针。
function mountWorkInProgressHook() {
const hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// 链表的首个节点
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 追加到已有链表末尾
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
2. 更新阶段(Update)
当组件因为 setState 重新执行时,Dispatcher 切换为 HooksDispatcherOnUpdate,此时调用的是 updateWorkInProgressHook():
- 从已有的
currentFiber.memoizedState(上一次渲染完成的 Fiber 链表)中,按顺序取出对应的旧 Hook 节点; - 克隆或复用该 Hook,读取其保存的
memoizedState和未决的更新队列queue; - 处理所有积压的更新 Action,计算出新的状态并返回。
function updateWorkInProgressHook() {
let nextCurrentHook;
if (currentHook === null) {
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState; // 从旧 Fiber 根部读取
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next; // 顺着链表后移一位
}
currentHook = nextCurrentHook;
// 创建对应的 WorkInProgress Hook 副本并按序推进
const newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
return workInProgressHook;
}
useEffect 的环形 Effect 链表机制
除了状态 Hook,useEffect 产生的副作用(Effects)还会被打包成一个独立的环形链表,挂载在 fiber.updateQueue 上。
在 Commit 阶段:
- Before Mutation / Mutation 阶段:React 处理 DOM 节点的增删改;
-
Layout 阶段:同步执行
useLayoutEffect的销毁与挂载回调; -
Passive Effects 阶段(异步微任务/宏任务):通过 Scheduler 异步调度,执行
useEffect的清理函数并触发新的 effect 回调,绝不阻塞浏览器的首屏绘制。
看清了底层单向链表的数据流动,你就能从根本上理解 React Hooks 极其纯粹的函数式设计哲学与严苛约束背后的架构必然。
到此这篇关于React Hooks 链表结构与更新挂载深度解析的文章就介绍到这了,更多相关React Hooks 链表结构内容请搜索IT俱乐部以前的文章或继续浏览下面的相关文章希望大家以后多多支持IT俱乐部!
