Hooks在Fiber上的存储原理

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: 0queue 持有 setCount 的更新函数。
  • Hook2 :存储 name: 'John'
  • Hook3:存储 effect 对象(依赖、销毁函数、创建函数)。

2. 更新渲染:复用链表

当组件更新时,React 会按照相同的顺序再次执行 Hook 调用。此时:

  • workInProgress Fiberalternate(对应 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,   // 更新优先级
};

工作流程

  1. 用户调用 setCount(1) → 创建 Update 对象,加入 queue.pending 环形链表。
  2. 触发组件重新渲染 → React 进入更新阶段,遍历 queue 中的 Updates,计算新的 state
  3. 计算结果存入 hook.memoizedState
  4. 清空 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 或错位,引发内部状态错乱。

七、性能与内存优化点

  1. 链表复用:更新时克隆旧 Hook,避免重复创建对象,但会保留旧值的引用(帮助 GC 判断)。
  2. 环形更新队列pending 指向最后一个更新,插入复杂度 O(1)。
  3. 双缓存隔离 :current Fiber 和 workInProgress Fiber 的 Hooks 链表通过 alternate 隔离,避免并发更新干扰。
  4. Effect 清理 :commit 阶段执行上一个 Effect 的 destroy 函数,再执行新的 create,确保资源释放。

总结:Hooks 本质上是函数组件状态的「外部化存储」------ 状态不保存在函数闭包内,而是挂在 Fiber 节点上,通过链表保证调用顺序,通过更新队列管理异步更新,最终实现函数组件的状态持久化。

如果需要,我可以进一步解释 useState 的更新优先级机制useEffect 与 useLayoutEffect 在 commit 阶段的执行时机差异

相关推荐
you45801 小时前
学成在线--day02 CMS前端开发(含Vue基础知识得回顾)
前端·javascript·vue.js
xiaofeichaichai1 小时前
虚拟 DOM
前端·javascript·vue.js
2401_878454531 小时前
前端高频得手写题
前端
初一初十2 小时前
vue3实现的纯前端护肤品商城网站
前端·javascript·vue.js·前端框架
卷帘依旧2 小时前
React状态管理方案怎么选
前端
zeqinjie2 小时前
Flutter 折叠屏 iPad / 宽屏适配实践
android·前端·flutter
小村儿2 小时前
连载13- 内部Tools,Claude Code 怎么真正"动"你的代码
前端·后端·ai编程
IT_陈寒2 小时前
Python的线程池把我坑惨了,原来异步不是万能的
前端·人工智能·后端
初一初十3 小时前
vue3茶叶商城网站vue网页vuejs前端
前端·javascript·vue.js·vscode·前端框架