人人都会AI编程

7.2 Hooks 的底层存储:链表结构与调用顺序约束

更新时间:2026-07-10

当你第一次在组件中使用 useStateuseEffect 等 Hooks 时,React 会在内存中为这个组件的 Fiber 节点上构建一条链表。这条链表精确记录了组件内所有 Hooks 的调用顺序和数据,是 Hooks 正常工作的根基。

为什么 Hooks 不能放在条件或循环中

React 强制要求 Hooks 必须按相同的顺序、在每次渲染时都调用。根本原因正是因为 React 依赖调用顺序来查找对应的 Hook 数据。如果某次渲染跳过了某个 Hook(例如放在 if 里),链表对应位置的节点就会错位,后面的所有 Hook 都会读取到错误的状态,导致难以排查的 bug。

// ❌ 错误示例:条件性调用 Hooks
function BadComponent({ condition }) {
  if (condition) {
    const [name, setName] = useState(''); // 违反规则
  }
  const [age, setAge] = useState(0);      // 如果 condition 从 true 变 false,
                                           // age 会错误地读取到 name 的状态
}

链表在 Fiber 中的存储位置

每个函数组件在 Fiber 架构中都有一个 memoizedState 属性,它指向该组件 Hooks 链表的第一个节点。当组件首次渲染时(mount 阶段),React 会顺序调用所有 Hooks 并创建链表节点;后续更新时(update 阶段),会复用这个链表,按照调用顺序依次取出对应的节点

Fiber 节点
   |
   v
memoizedState ──> Hook 1 (useState) ──next──> Hook 2 (useEffect) ──next──> Hook 3 (useRef) ──> null

每个 Hook 节点都是一个对象,包含该 Hook 所需的所有信息,例如:

  • memoizedState:当前 Hook 的状态值(useState 存的是状态值,useEffect 存的是副作用链表等)
  • queue:更新队列,存放后续 setState 的更新函数
  • next:指向下一个 Hook 节点,形成链表

首次渲染(mount)与更新(update)的流程

Mount 阶段:React 按顺序执行组件函数,每次遇到一个 useXxx(),就在链表尾部追加一个新节点,并将当前值存入节点。组件执行完成后,一条完整的 Hooks 链表就挂在 Fiber 上了。

Update 阶段:React 再次执行组件函数,此时 Hooks 链表已经存在。React 内部维护一个当前正在处理的指针,从上一次链表的头部开始,依次取出对应的节点。例如第一次调用 useState() 就对应链表第一个节点,第二次调用对应第二个节点,依此类推。更新时只修改对应节点的 memoizedStatequeue,不会改变链表结构。

一个简化版的 Hooks 链表实现

为了让你直观理解,以下是一个极简的模拟实现,展示了 React 内部如何在 mount 和 update 时管理 Hooks 链表:

let currentFiber = null;
let hookIndex = 0; // 用于 update 时顺序遍历

function mountWorkInProgressHook() {
  const hook = {
    memoizedState: null,
    queue: [],
    next: null,
  };
  // 找到链表尾部并追加
  if (!currentFiber.memoizedState) {
    currentFiber.memoizedState = hook;
  } else {
    let lastHook = currentFiber.memoizedState;
    while (lastHook.next) {
      lastHook = lastHook.next;
    }
    lastHook.next = hook;
  }
  return hook;
}

function updateWorkInProgressHook() {
  let hook = currentFiber.memoizedState;
  // 按照 hookIndex 找到对应位置的节点
  let i = 0;
  while (i < hookIndex && hook !== null) {
    hook = hook.next;
    i++;
  }
  hookIndex++;
  return hook;
}

在真实的 React 源码中,Hooks 链表的管理远比这个复杂,还涉及 workInProgress 树、更新队列的批处理、优先级管理等,但核心思路是一致的:用链表顺序来匹配 Hooks 调用

调用顺序约束的实际影响

因为链表是按调用顺序匹配的,所以下面这段代码会引发灾难:

function BuggyComponent() {
  const [count, setCount] = useState(0);
  
  if (count > 3) {
    // 当 count > 3 时,这个 useEffect 才执行,链表里会多出一个节点
    useEffect(() => {
      document.title = `Count: ${count}`;
    });
  }

  const [name, setName] = useState('');
  // 当 count <= 3 时,上面没有 useEffect,name 的 Hook 对应链表第二个节点
  // 当 count > 3 时,链表有了 useEffect,name 的 Hook 变成第三个节点,
  // 它会错误地读取到 useEffect 的状态,导致崩溃。
}

这就是为什么 React 官方将“只在最顶层使用 Hooks”作为一条铁律,并且 ESLint 插件 eslint-plugin-react-hooks 能自动检测此类违规。

总结

Hooks 的底层存储本质就是一个简单的单向链表,它用顺序代替了显式的 key,使得 API 像普通函数调用一样简洁。理解这一点能帮助你彻底掌握 Hooks 的规则,避免写出难以调试的 bug,也为深入自定义 Hooks 和性能优化打下基础。