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: 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,   // 更新优先级
};

工作流程:

  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 阶段的执行时机差异。

相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万3 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝3 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋3 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁3 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95273 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大3 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师3 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学3 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端