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;
}有一个重要的细节:dispatch 函数通过 bind 绑定了 Fiber 节点和更新队列。这就是为什么 setState 可以在组件外部被调用(比如在 setTimeout 中)——它已经"记住"了要更新哪个组件。
7.3.2 Update 阶段:处理更新
当组件因为 setState 被调用而重新渲染时,useState 调用的是 updateState:
typescript
function updateState<S>(initialState: (() => S) | S): [S, Dispatch<SetStateAction<S>>] {
// useState 在 update 阶段本质上就是一个 useReducer
return updateReducer(basicStateReducer, initialState);
}
// useState 的 reducer 就是这么简单
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function'
? (action as (S) => S)(state)
: action;
}这揭示了一个重要事实:useState 在内部就是一个使用了 basicStateReducer 的 useReducer。
typescript
function updateReducer<S, A>(
reducer: (S, A) => S,
initialArg: S
): [S, Dispatch<A>] {
// 1. 获取当前 Hook(从链表中按顺序取下一个)
const hook = updateWorkInProgressHook();
const queue = hook.queue!;
queue.lastRenderedReducer = reducer;
const current = currentHook!;
let baseQueue = current.baseQueue;
// 2. 将 pending 更新合并到 baseQueue
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
if (baseQueue !== null) {
// 合并两个环形链表
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
// 3. 逐个处理更新队列中的 update
if (baseQueue !== null) {
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
const updateLane = update.lane;
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// 优先级不够,跳过此更新(保留到下次)
const clone = {
lane: updateLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: null as any,
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
} else {
// 优先级足够,计算新状态
if (update.hasEagerState) {
// 使用预计算的状态(性能优化)
newState = update.eagerState;
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
hook.memoizedState = newState;
hook.baseState = newBaseState ?? newState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
return [hook.memoizedState, queue.dispatch!];
}7.3.3 dispatchSetState:更新的触发
当你调用 setCount(count + 1) 时,真正执行的是 dispatchSetState:
typescript
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A
) {
// 1. 获取更新优先级
const lane = requestUpdateLane(fiber);
// 2. 创建 update 对象
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: null as any,
};
if (isRenderPhaseUpdate(fiber)) {
// 在渲染过程中调用 setState(render phase update)
enqueueRenderPhaseUpdate(queue, update);
} else {
// 3. 🔑 关键优化:Eager State
const alternate = fiber.alternate;
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// 当前没有其他待处理的更新
// 可以立即计算新状态,如果和旧状态相同就跳过调度
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
const currentState = queue.lastRenderedState;
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (Object.is(eagerState, currentState)) {
// 新旧状态相同,无需调度更新!
enqueueUpdate(fiber, queue, update, lane);
return;
}
}
}
// 4. 将 update 加入队列
enqueueUpdate(fiber, queue, update, lane);
// 5. 调度更新
const root = scheduleUpdateOnFiber(fiber, lane);
}
}