Appearance
第7章 Hooks 的实现原理
本章要点
- Hooks 的数据结构:单向链表与 Fiber 节点的绑定关系
- useState 的完整实现:mount 阶段与 update 阶段的差异
- useReducer 的工作机制:与 useState 的同源性
- useEffect 与 useLayoutEffect 的内部调度差异
- useRef 为什么是最简单的 Hook——以及它为什么不触发重渲染
- useMemo 与 useCallback 的缓存策略与失效机制
- useContext 的订阅模型与性能陷阱
- Hook 规则的技术根因:为什么不能条件调用 Hook
- Dispatcher 切换机制:React 如何在不同阶段使用不同的 Hook 实现
Hooks 是 React 16.8 引入的最重大的范式变革。表面上,它让函数组件拥有了状态和副作用的能力;但从内核的角度看,Hooks 的设计远比 API 表面呈现的要精妙得多。
当你写下 const [count, setCount] = useState(0) 时,你可能会好奇:一个看似无状态的函数,每次调用都从头执行,它是怎么"记住"上一次的值的?更关键的是,当同一个组件中有多个 useState,React 是怎么知道哪个 state 对应哪个调用的?
答案藏在两个核心设计中:Fiber 节点上的 Hook 链表和基于调用顺序的索引机制。
7.1 Hook 的数据结构
每个 Hook 在 React 内部对应一个 Hook 对象,这些对象通过 next 指针串成一条单向链表,挂载在 Fiber 节点的 memoizedState 属性上:
typescript
// Hook 的核心数据结构
type Hook = {
memoizedState: any; // 当前状态值(不同 Hook 存储不同的东西)
baseState: any; // 基准状态(用于并发模式下的状态计算)
baseQueue: Update<any> | null; // 上次未处理完的更新队列
queue: UpdateQueue<any> | null; // 当前待处理的更新队列
next: Hook | null; // 指向下一个 Hook
};
// 不同 Hook 的 memoizedState 存储的内容
// useState → state 值
// useReducer → state 值
// useEffect → Effect 对象
// useRef → { current: value }
// useMemo → [cachedValue, deps]
// useCallback → [callback, deps]
// useContext → context 的当前值(但实际上不使用 Hook 链表)当 React 渲染一个函数组件时,Hook 链表的构建过程如下:
tsx
function MyComponent() {
const [name, setName] = useState('React'); // Hook 1
const [count, setCount] = useState(0); // Hook 2
const ref = useRef(null); // Hook 3
useEffect(() => { /* ... */ }, [count]); // Hook 4
const memoized = useMemo(() => count * 2, [count]); // Hook 5
return <div>{name}: {count} (x2 = {memoized})</div>;
}图 7-1:Hook 链表结构示意图
7.2 Dispatcher:Hook 的两张面孔
React 中的每个 Hook API(如 useState)并不是一个固定的实现,而是一个"门面",它的实际行为取决于当前的 Dispatcher。React 维护了一个全局变量 ReactCurrentDispatcher,在不同的执行阶段会切换到不同的 Dispatcher:
typescript
// React 的 Dispatcher 机制
const ReactCurrentDispatcher = {
current: null as Dispatcher | null,
};
// 三种主要的 Dispatcher
const HooksDispatcherOnMount: Dispatcher = {
useState: mountState,
useEffect: mountEffect,
useRef: mountRef,
useMemo: mountMemo,
useCallback: mountCallback,
useReducer: mountReducer,
useContext: readContext,
// ...
};
const HooksDispatcherOnUpdate: Dispatcher = {
useState: updateState,
useEffect: updateEffect,
useRef: updateRef,
useMemo: updateMemo,
useCallback: updateCallback,
useReducer: updateReducer,
useContext: readContext,
// ...
};
const InvalidHooksDispatcher: Dispatcher = {
useState: throwInvalidHookError,
useEffect: throwInvalidHookError,
// ... 所有方法都抛错
};当 React 开始渲染一个函数组件时,会根据情况设置 Dispatcher:
typescript
function renderWithHooks(
current: Fiber | null,
workInProgress: Fiber,
Component: Function,
props: any
) {
// 根据是首次渲染还是更新,设置不同的 Dispatcher
if (current !== null && current.memoizedState !== null) {
// 更新阶段
ReactCurrentDispatcher.current = HooksDispatcherOnUpdate;
} else {
// 首次挂载
ReactCurrentDispatcher.current = HooksDispatcherOnMount;
}
// 执行函数组件
let children = Component(props);
// 渲染完成后,将 Dispatcher 设为无效版本
// 这就是为什么在组件外调用 Hook 会报错
ReactCurrentDispatcher.current = InvalidHooksDispatcher;
return children;
}这就是为什么在组件渲染函数外部调用 useState 会得到一个"Invalid hook call"错误——此时的 Dispatcher 已经被切换为 InvalidHooksDispatcher,所有 Hook 调用都会抛出异常。
7.3 useState 的完整实现
useState 是使用最广泛的 Hook,它的实现也是理解所有 Hook 工作原理的基石。
7.3.1 Mount 阶段:初始化
首次渲染时,useState 调用的是 mountState:
typescript
function mountState<S>(initialState: (() => S) | S): [S, Dispatch<SetStateAction<S>>] {
// 1. 创建一个新的 Hook 对象并添加到链表末尾
const hook = mountWorkInProgressHook();
// 2. 处理初始值(支持函数形式的惰性初始化)
if (typeof initialState === 'function') {
initialState = (initialState as () => S)();
}
// 3. 设置初始状态
hook.memoizedState = initialState;
hook.baseState = initialState;
// 4. 创建更新队列
const queue: UpdateQueue<S> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState,
};
hook.queue = queue;
// 5. 创建 dispatch 函数(即 setState)
const dispatch = (queue.dispatch = dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue
));
return [hook.memoizedState, dispatch];
}
// mountWorkInProgressHook 负责创建 Hook 对象并链接到链表
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// 这是链表的第一个 Hook
currentlyRenderingFiber.memoizedState = hook;
workInProgressHook = hook;
} else {
// 追加到链表末尾
workInProgressHook.next = hook;
workInProgressHook = hook;
}
return hook;
}