Hooks链表与调用顺序
一句话概括
React Hooks 的底层不是魔法,而是一条挂在 fiber.memoizedState 上的单向链表——每个 Hook 通过 next 指针串联,渲染时按调用顺序遍历链表读取状态。一旦条件执行破坏顺序,链表索引错位,所有状态全崩。
核心知识点
1. Hook 链表长什么样
每个组件 fiber 上的 memoizedState 不是存 state 值,而是指向一条 Hook 单链表:
1
2
3
4
// fiber.memoizedState 的结构
const hook1 = { memoizedState: 0, next: hook2 }; // useState(0)
const hook2 = { memoizedState: 'hi', next: hook3 }; // useState('hi')
const hook3 = { memoizedState: {...effect...}, next: null }; // useEffect
关键:React 不按”名称”找 Hook,而是按”第几个调用的”。第 3 次调用就是链表的第 3 个节点。
2. mount 创建 vs update 读取——两套分发器
React 通过一个全局 dispatcher 做 mount/update 分支:
1
2
3
4
5
6
7
8
9
10
11
12
13
// mount 时:创建节点挂到链表尾部
function mountState(initial) {
const hook = mountWorkInProgressHook(); // 链表尾部追加
hook.memoizedState = initial;
return [hook.memoizedState, dispatch];
}
// update 时:按顺序取下一个节点
function updateState() {
const hook = updateWorkInProgressHook(); // 推进游标
// 处理 queue 中的更新,返回最新 memoizedState
return [hook.memoizedState, hook.queue.dispatch];
}
mount 追加,update 推进游标——同一个 useState 调用,不同阶段做不同的事。
3. 为什么条件调用是灾难
1
2
3
4
5
6
7
function Bug({ flag }) {
const [a] = useState(0); // 链表节点 0
if (flag) {
const [b] = useState('x'); // 链表节点 1(flag=true 时存在)
}
const [c] = useState(2); // 链表节点 2(flag=true)/ 节点 1(flag=false)
}
| 渲染 | 调用序列 | 实际读到的 |
|---|---|---|
| flag=true | useState(0)→useState(‘x’)→useState(2) | ✅ a→b→c |
| flag=false | useState(0)→(跳过)→useState(2) | ❌ a→c→?错位 |
第二次渲染 useState(2) 读到了原来 useState('x') 的节点,返回字符串而非数字。更致命的是 useEffect 会跑错位置。
4. 自定义 Hook 不享有”豁免权”
1
2
3
4
5
6
7
8
9
10
11
function useWindowSize() {
const [size] = useState({}); // 直接挂到调用方组件的链表上
useEffect(() => { /*...*/ }, []);
return size;
}
function App() {
const [a] = useState(0); // 链表节点 0
const size = useWindowSize(); // 内部展开了 useState + useEffect → 节点 1, 2
const [b] = useState(1); // 链表节点 3
}
自定义 Hook 内的 Hooks 被内联展开到调用方链表上,同样要遵守顺序规则。
5. setState 函数为什么引用稳定
1
2
3
4
5
6
// mount 时 queue.dispatch 指向同一个函数 → 引用永不变
const queue = { pending: null, dispatch: null };
queue.dispatch = dispatchSetState.bind(null, fiber, queue);
hook.queue = queue;
// update 时直接从 hook.queue 取出 dispatch → 同一个引用
这就是为什么 setState 不需要放在 useEffect 依赖数组中——它是引用稳定的。
「其实你每天都在用」
输入框即时搜索:每次按键
setSearchQuery,React 按顺序找到链表中的对应节点,返回最新值。背后是链表游标在步进。表单多字段管理:
const [name, setName] = useState(''); const [email, setEmail] = useState('');每个字段一个 Hook,链表里是相邻两个节点,写入互不干扰。第三方 Hook 库:
useSWR('/api/data')、useDebounce(value)这些库内部的useState/useEffect被”内联”到你的组件链表里,和你的 Hooks 共享同一条链。React DevTools 看到的状态列表:DevTools 遍历
fiber.memoizedState → next → next把链表读出来展示。你在面板里看到的 Hooks 顺序就是链表的物理顺序。ESLint 报错
Hooks must be called in the exact same order:就是 eslint-plugin-react-hooks 用 AST 分析检测到你的 Hook 调用了if/for包裹,链表顺序将被破坏。
常见误解 (FAQ)
❌ 误区 1:”多个同名 useState 靠变量名区分”
React 不看变量名 count 还是 name,只看调用位置(第一次、第二次……)。你把两行 useState 交换位置,所有状态全乱。
❌ 误区 2:”useCallback/useMemo 不用遵守顺序规则”
所有 Hooks(包括 useRef、useMemo、useCallback、useReducer)都挂在同一条链表上,都靠调用顺序定位。没有例外。
❌ 误区 3:”如果条件分支的 Hook 数量相同就安全”
即使数量相同,不同渲染中调用的具体 Hook 不同(比如 flag ? useState(0) : useReducer(fn, 0)),依然会错位——React 从链表拿到的节点和预期类型不匹配。
❌ 误区 4:”StrictMode 下 Hooks 执行两次是 bug”
这是 React 18 的故意行为,用于检测副作用是否纯。组件函数体和 useState 的初始化函数都会被双调用,方便暴露不纯的逻辑。
一句话总结
Hooks 的本质是一条隐式的单链表,调用顺序就是它的”身份证”——理解了这条链,你就理解了一切 Hooks 规则的根源。