React Hooks 在 Fiber 上的存储原理,核心可以概括为一句话:Hooks 是以链表形式存储在对应 Fiber 节点的 memoizedState 属性上的。
下面从数据结构、存储过程、工作原理三个层面展开。
一、核心数据结构
每个 Fiber 节点有两个关键字段存储 Hooks 相关状态:
javascript
// Fiber 节点简化结构
{
memoizedState: Hook, // 当前 Fiber 的 Hooks 链表头指针
updateQueue: UpdateQueue, // 存放 useEffect、useLayoutEffect 的回调队列
}
每个 Hook 本身也是一个对象,通过 next 指针形成单向链表:
javascript
// Hook 数据结构(简化)
type Hook = {
memoizedState: any, // 当前 Hook 存储的状态值(useState的值、useReducer的state、useMemo的缓存值)
baseState: any, // 基础状态(用于优先级更新)
baseQueue: Update, // 基础更新队列
queue: UpdateQueue, // 更新队列(存放 setXxx 触发的更新)
next: Hook | null, // 指向下一个 Hook(链表连接的关键)
};
二、Hooks 如何存储在 Fiber 上
1. 首次渲染:构建链表
当函数组件首次执行时,React 会按顺序执行每个 Hook 调用,并为每个 Hook 创建一个节点,串成链表挂载到 fiber.memoizedState。
javascript
function MyComponent() {
const [count, setCount] = useState(0); // Hook1
const [name, setName] = useState('John'); // Hook2
useEffect(() => {}, []); // Hook3
return <div>{count}</div>;
}
执行后 fiber.memoizedState 指向的链表结构:
yaml
fiber.memoizedState → Hook1 → Hook2 → Hook3 → null
↓ ↓ ↓
memoizedState: 0 'John' effect对象
queue: 更新队列 更新队列 queue: 依赖队列
- Hook1 :存储
count: 0,queue持有setCount的更新函数。 - Hook2 :存储
name: 'John'。 - Hook3:存储 effect 对象(依赖、销毁函数、创建函数)。
2. 更新渲染:复用链表
当组件更新时,React 会按照相同的顺序再次执行 Hook 调用。此时:
- 从
workInProgress Fiber的alternate(对应 current Fiber)上取出旧的 Hook 链表。 - 克隆旧的 Hook 节点,复用其数据结构,但更新
memoizedState。 - 关键约束:Hook 的调用顺序必须和上次完全一致,否则链表匹配失败(这就是为什么 Hooks 不能在条件/循环中调用)。
javascript
// 简化的更新逻辑
function updateWorkInProgressHook() {
let nextCurrentHook = currentFiber.memoizedState; // 旧的链表头
let nextWorkInProgressHook = workInProgressFiber.memoizedState; // 正在构建的新链表
if (nextWorkInProgressHook !== null) {
// 已经有 workInProgress Hook,直接复用
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
nextCurrentHook = nextCurrentHook?.next;
} else {
// 首次进入该 Fiber,克隆 current hook
const newHook = cloneHook(currentHook);
workInProgressFiber.memoizedState = newHook;
}
return workInProgressHook;
}
三、Hook 的更新存储(queue 机制)
每个 useState / useReducer 的 Hook 都有一个 queue 属性,用于存储未处理的更新。
javascript
type UpdateQueue = {
pending: Update, // 环状链表,指向最后一次更新
dispatch: any, // setCount 函数本身
lastRenderedReducer: Reducer,
lastRenderedState: any,
};
type Update = {
action: any, // setCount 的参数,如 count => count + 1
next: Update, // 指向下一个更新(形成环状链表)
priority: number, // 更新优先级
};
工作流程:
- 用户调用
setCount(1)→ 创建Update对象,加入queue.pending环形链表。 - 触发组件重新渲染 → React 进入更新阶段,遍历
queue中的 Updates,计算新的state。 - 计算结果存入
hook.memoizedState。 - 清空
queue.pending(或标记已处理)。
环形链表优化 :所有 Update 通过 next 指针首尾相连,pending 指向最后一个,方便快速插入和遍历。
四、Effect 的存储(updateQueue)
useEffect / useLayoutEffect / useImperativeHandle 的副作用不会存储在 memoizedState 的链表里,而是单独存在 fiber.updateQueue 上。
javascript
// fiber.updateQueue 结构(简化)
{
lastEffect: Effect, // 环形链表的最后一个 Effect
effects: Effect[], // 扁平化的 effect 列表(commit 阶段遍历)
}
type Effect = {
tag: HookFlags, // 标记是 useEffect(0) 还是 useLayoutEffect(4)
create: () => () => void,// effect 回调函数
destroy: () => void, // 上一次的清理函数
deps: any[], // 依赖数组
next: Effect, // 指向下一个 effect(环形链表)
};
为何分开存储?
memoizedState链表只存储状态相关的 Hook(state、ref、context)。updateQueue专门存储副作用,方便在 commit 阶段统一处理(异步执行 useEffect,同步执行 useLayoutEffect)。- 副作用链表是环形结构,
lastEffect.next === lastEffect表示只有一个 Effect。
五、图解完整存储关系
yaml
Fiber
├── memoizedState → Hook1 (useState)
│ ├── memoizedState: 0
│ ├── queue: { pending: update → ... }
│ └── next → Hook2 (useEffect)
│ ├── memoizedState: { create, deps }
│ └── next → null
│
└── updateQueue → { lastEffect: Effect2 }
└── Effect2 (useEffect)
├── create: () => cleanup
├── deps: []
└── next → Effect2 (自环)
六、为什么不能打破调用顺序?
现在可以精确回答这个问题:
React 内部通过一个游标 (currentlyRenderingFiber + workInProgressHook 指针)来追踪当前执行到第几个 Hook。
javascript
let currentlyRenderingFiber: Fiber;
let workInProgressHook: Hook | null;
function useState(initialState) {
const hook = updateWorkInProgressHook(); // 每次调用,指针后移一位
// ... 处理更新
return [hook.memoizedState, hook.queue.dispatch];
}
如果在条件分支中调用 Hook:
javascript
// ❌ 错误示例
if (someCondition) {
useState(0); // 条件满足时执行
}
useState(1); // 条件不满足时执行
- 首次渲染 :
someCondition = true→ 链表为[HookA, HookB] - 二次渲染 :
someCondition = false→ React 期望链表仍然是[HookA, HookB],但实际调用只有HookB,导致workInProgressHook指向 null 或错位,引发内部状态错乱。
七、性能与内存优化点
- 链表复用:更新时克隆旧 Hook,避免重复创建对象,但会保留旧值的引用(帮助 GC 判断)。
- 环形更新队列 :
pending指向最后一个更新,插入复杂度 O(1)。 - 双缓存隔离 :current Fiber 和 workInProgress Fiber 的 Hooks 链表通过
alternate隔离,避免并发更新干扰。 - Effect 清理 :commit 阶段执行上一个 Effect 的
destroy函数,再执行新的create,确保资源释放。
总结:Hooks 本质上是函数组件状态的「外部化存储」------ 状态不保存在函数闭包内,而是挂在 Fiber 节点上,通过链表保证调用顺序,通过更新队列管理异步更新,最终实现函数组件的状态持久化。
如果需要,我可以进一步解释 useState 的更新优先级机制 或 useEffect 与 useLayoutEffect 在 commit 阶段的执行时机差异。