Hooks实现原理
React Hooks 的实现原理是基于链表结构和闭包机制,通过严格的调用顺序来管理状态。以下是其核心原理的详细解析:
1. 基本数据结构:链表
每个函数组件内部维护一个 Fiber 节点,其中包含一个 memoizedState
属性,它是一个链表,每个节点对应一个 Hook:
// 简化的 Fiber 节点结构
const fiber = {
type: FunctionComponent, // 组件类型
stateNode: null, // 组件实例
memoizedState: null, // Hook 链表头节点
// ...
};
// 简化的 Hook 节点结构
const hook = {
memoizedState: initialState, // 状态值
baseState: null, // 基础状态(用于并发更新)
queue: null, // 更新队列
next: null, // 指向下一个 Hook 的指针
};
2. useState 的实现原理
(1) 首次渲染
- 创建一个 Hook 节点并添加到链表中。
- 执行初始状态计算(如果是函数,则调用该函数)。
- 返回状态值和更新函数。
// 简化的 useState 实现
function useState(initialState) {
// 获取当前 Fiber 节点
const fiber = getCurrentFiber();
// 从链表中获取或创建当前 Hook
let hook;
if (!fiber.memoizedState) {
// 首次渲染:创建新 Hook
hook = {
memoizedState: initialState,
queue: { pending: null },
next: null,
};
fiber.memoizedState = hook;
} else {
// 后续渲染:获取当前 Hook
hook = fiber.currentHook;
fiber.currentHook = hook.next;
}
// 处理更新队列
const queue = hook.queue;
if (queue.pending) {
// 计算新状态(合并所有更新)
let newState = hook.memoizedState;
let update = queue.pending.next; // 循环链表的第一个更新
do {
newState = update.action(newState);
update = update.next;
} while (update !== queue.pending.next);
hook.memoizedState = newState;
queue.pending = null;
}
// 返回状态和更新函数
const setState = (action) => {
// 创建新的更新并添加到队列
const update = {
action,
next: null,
};
// 将更新添加到循环链表
if (queue.pending === null) {
update.next = update; // 自己指向自己
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
// 调度重新渲染
scheduleUpdate();
};
return [hook.memoizedState, setState];
}
(2) 后续渲染
- 按顺序从链表中获取对应的 Hook 节点。
- 处理更新队列,计算新状态。
- 返回最新状态值和更新函数。
3. useEffect 的实现原理
(1) 数据结构
每个 useEffect
对应一个 Hook 节点,包含回调函数、依赖项和清理函数:
const effectHook = {
tag: 'effect', // 标记为 effect
create: effectCallback, // 副作用回调函数
destroy: null, // 清理函数(由 create 返回)
deps: dependencies, // 依赖项数组
next: null, // 指向下一个 effect
};
(2) 执行时机
- 首次渲染:将 effect 添加到队列,不立即执行。
- 提交阶段后:异步执行所有 effect(按顺序)。
- 依赖项变化:先执行上一次的清理函数,再执行新的 effect。
// 简化的 useEffect 实现
function useEffect(create, deps) {
const fiber = getCurrentFiber();
const hook = getCurrentHook();
// 比较依赖项
const prevDeps = hook.deps;
let hasChanged = true;
if (prevDeps) {
hasChanged = deps.some((dep, i) => dep !== prevDeps[i]);
}
if (hasChanged) {
// 保存新的 effect
hook.create = create;
hook.deps = deps;
// 将 effect 添加到 fiber 的 effects 链表
if (!fiber.effects) {
fiber.effects = [hook];
} else {
fiber.effects.push(hook);
}
}
}
// 提交阶段后执行所有 effects
function commitHookEffects() {
const fiber = getCurrentFiber();
// 执行所有 effect 的清理函数(如果有)
fiber.effects.forEach(effect => {
if (effect.destroy) {
effect.destroy();
}
});
// 执行所有 effect 的回调函数
fiber.effects.forEach(effect => {
effect.destroy = effect.create();
});
}
4. 严格的调用顺序
Hooks 依赖调用顺序来正确定位状态:
function Component() {
useState(1); // 第一个 Hook
useEffect(() => {}); // 第二个 Hook
useState(2); // 第三个 Hook
// React 通过调用顺序(1→2→3)来管理状态
}
为什么不能在条件语句中使用 Hook?
如果 Hook 调用顺序发生变化(如在条件语句中),会导致状态混乱:
function Component() {
useState(1); // 第一个 Hook
if (condition) {
useEffect(() => {}); // 可能是第二个或跳过
}
useState(2); // 取决于条件,可能是第二个或第三个
// 🔥 错误:调用顺序不一致,导致状态混乱
}
5. 闭包陷阱与解决方案
闭包陷阱示例
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
console.log(count); // 始终打印 0(闭包捕获初始值)
}, 1000);
}, []);
return <button onClick={() => setCount(count + 1)}>Click</button>;
}
解决方案
-
使用函数式更新:
setCount(prev => prev + 1); // 不依赖闭包中的 count
-
使用 useRef 保存最新值:
const countRef = useRef(count);
countRef.current = count; // 每次渲染更新
useEffect(() => {
setInterval(() => {
console.log(countRef.current); // 始终获取最新值
}, 1000);
}, []);
6. 自定义 Hook 的复用机制
自定义 Hook 本质是普通函数,但通过相同的调用顺序规则,可以复用状态逻辑:
function useCounter(initialValue) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(c => c + 1);
return { count, increment };
}
function ComponentA() {
const { count, increment } = useCounter(0); // 第一个 useCounter
// 对应第一个 useState
}
function ComponentB() {
const { count, increment } = useCounter(10); // 第二个 useCounter
// 对应另一个 useState(与 ComponentA 独立)
}
总结
React Hooks 的实现核心是:
- 链表结构:每个函数组件对应一个 Fiber 节点,其中的
memoizedState
是一个链表,存储所有 Hook 状态。 - 调用顺序:Hooks 依赖严格的调用顺序来定位状态,因此不能在条件语句中使用。
- 异步执行:
useEffect
在提交阶段后异步执行,避免阻塞渲染。