React Hooks链表

15 阅读3分钟

React Hooks 的链表结构以及为什么必须在顶层调用

为了直观理解,我们可以抛开复杂的源码,用“没有任何标签的抽屉柜”**这个比喻来解释。

1. 核心机制:盲人管家与无标签抽屉

想象 React 是一个“盲人管家”,而组件是一个“立柜”。

  • Hook 的本质:当写 useStateuseEffect 时,相当于让管家在这个立柜里按顺序安装一个抽屉来存放数据。
  • 关键点:管家看不见你给变量起的名(如 count, userList),他只记得顺序(第1个、第2个、第3个)。

第一次渲染(初始化):贴条形码

当第一次运行组件时,代码从上到下执行:

  1. 第1行 useState('A'):管家装上第1个抽屉,里面放了 'A'。
  2. 第2行 useState('B'):管家装上第2个抽屉,里面放了 'B'。
  3. 第3行 useEffect(...):管家装上第3个抽屉,里面放了副作用的处理逻辑。

此时,React 在内存里建立了一个链表(就像一串用绳子连起来的抽屉):

Node1 ('A') --> Node2 ('B') --> Node3 (Effect) 〔3〕〔21〕


2. 为什么要按顺序?(重渲染时的灾难)

正常情况:

当组件更新时,React 再次从头执行函数代码。管家会按顺序拉开抽屉拿出数据:

  1. 代码走到第1个 Hook -> 管家拉开第1个抽屉 -> 拿到 'A'。
  2. 代码走到第2个 Hook -> 管家拉开第2个抽屉 -> 拿到 'B'。 一切正常。

异常情况(如果在条件语句里写 Hook):

假设你写了这样一个代码,且本次渲染中 if 条件为 false

// ❌ 错误示范
function MyComponent() {
  // 第1个 Hook
  const [name, setName] = useState('Alice'); 
  
  // 🔴 假设这次条件为 false,这个 Hook 被跳过了!
  if (showPassword) {
     const [password, setPassword] = useState('123456'); 
  }

  // 第3个 Hook(但在本次渲染中,它变成了第2个被执行的代码)
  useEffect(() => { ... }); 
}

此时发生的“车祸现场”:

  1. 代码执行第1个 Hook:管家拉开第1个抽屉
    • 结果:拿到 'Alice'。✅ 对应正确。
  2. 代码跳过了 if 里的 Hook
  3. 代码执行 useEffect:这是代码遇到的第2个 Hook 请求。于是管家习惯性地拉开第2个抽屉
    • 预期:代码想要 useEffect 的逻辑。
    • 实际:管家拿出了之前存的密码 '123456'(因为那是第2个抽屉里的东西)。
    • 后果崩溃! React 发现你要的是一个 Effect,结果给了一个 String,类型完全对不上,或者数据张冠李戴。〔4〕〔25〕

3. 技术视角:链表结构 (Linked List)

在 React 源码内部,并没有使用“变量名”作为索引,而是通过链表Linked List)来维护状态。

每个组件(Fiber Node)上挂载了一个 memoizedState 属性,它指向链表的头部:

  • 结构图示
    Hook1 (next) ──> Hook2 (next) ──> Hook3 (next) ──> null
    
  • 工作流程: React 内部有一个指针(workInProgressHook)。每当你调用一次 Hook(如 useState),指针就向后移动一位 currentHook = currentHook.next
  • 核心冲突: 如果代码逻辑里少调用了一个 Hook,指针移动的次数就变少了,导致后续所有的 Hook 全部与链表中的位置错位。就像你在扣衬衫扣子,如果你跳过了第2个扣眼,后面所有的扣子都会扣错位置。〔3〕〔5〕〔21〕

总结

  1. 链表结构:React 像穿糖葫芦一样,按创建顺序把所有 Hook 串成一个链表,不通过名字查找,只通过位置查找。
  2. 顶层规则:为了保证“代码执行的顺序”和“React 内存中链表的顺序”永远一一对应,你必须把 Hook 放在函数最顶层,严禁放在循环、条件判断或嵌套函数中。〔1〕〔8〕
  3. 关键点:React 像一个严格按照编号办事的管家,它只认抽屉的位置(顺序),不认抽屉的名字(变量名)。如果顺序被破坏,整个链条就会错位。