React Hooks 的链表结构以及为什么必须在顶层调用
为了直观理解,我们可以抛开复杂的源码,用“没有任何标签的抽屉柜”**这个比喻来解释。
1. 核心机制:盲人管家与无标签抽屉
想象 React 是一个“盲人管家”,而组件是一个“立柜”。
- Hook 的本质:当写
useState或useEffect时,相当于让管家在这个立柜里按顺序安装一个抽屉来存放数据。 - 关键点:管家看不见你给变量起的名(如
count,userList),他只记得顺序(第1个、第2个、第3个)。
第一次渲染(初始化):贴条形码
当第一次运行组件时,代码从上到下执行:
- 第1行
useState('A'):管家装上第1个抽屉,里面放了 'A'。 - 第2行
useState('B'):管家装上第2个抽屉,里面放了 'B'。 - 第3行
useEffect(...):管家装上第3个抽屉,里面放了副作用的处理逻辑。
此时,React 在内存里建立了一个链表(就像一串用绳子连起来的抽屉):
2. 为什么要按顺序?(重渲染时的灾难)
正常情况:
当组件更新时,React 再次从头执行函数代码。管家会按顺序拉开抽屉拿出数据:
- 代码走到第1个 Hook -> 管家拉开第1个抽屉 -> 拿到 'A'。
- 代码走到第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个 Hook:管家拉开第1个抽屉。
- 结果:拿到 'Alice'。✅ 对应正确。
- 代码跳过了
if里的 Hook。 - 代码执行
useEffect:这是代码遇到的第2个 Hook 请求。于是管家习惯性地拉开第2个抽屉。
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〕