当你第一次在组件中使用 useState、useEffect 等 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() 就对应链表第一个节点,第二次调用对应第二个节点,依此类推。更新时只修改对应节点的 memoizedState 和 queue,不会改变链表结构。
一个简化版的 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 和性能优化打下基础。