Hooks 链表:useState 和 useEffect 的底层实现
第27章:Hooks 链表:useState 和 useEffect 的底层实现
Hooks 不是魔法,它是对链表数据结构的精心运用——链表的稳定顺序,是函数组件能拥有持久化状态的根本保证。
本章核心问题:'不能在条件语句中调用 Hooks'这条规则背后的实现机制是什么? 读完本章你将理解:
- 每个 Hook 调用对应 Fiber 节点上链表中的一个节点,顺序必须固定
- 双 Dispatcher 设计:mount 和 update 使用完全不同的函数实现
- useState 底层就是 useReducer 加 basicStateReducer
Level 1 · 你需要知道的(1-3年经验)
Hook 对象:链表中的一个节点
每个 Hook 调用(useState、useEffect、useRef 等)在 Fiber 节点上对应链表中的一个节点,节点结构如下:
// packages/react-reconciler/src/ReactFiberHooks.js
// Hook 对象的结构
export type Hook = {
memoizedState: any, // 当前稳定状态(对于 useState 是当前值,对于 useEffect 是 effect 对象)
baseState: any, // 基础状态(用于计算 update 队列的最终值)
baseQueue: Update<any, any> | null, // 基础更新队列(未被处理的低优先级更新)
queue: UpdateQueue<any, any> | null, // 当前更新队列(环形链表)
next: Hook | null, // 指向下一个 Hook 节点
};
这些 Hook 节点以链表形式挂在 Fiber 节点的 memoizedState 字段上。以下面这个组件为例:
function Counter() {
const [count, setCount] = useState(0); // Hook 1
const [name, setName] = useState(''); // Hook 2
useEffect(() => { // Hook 3
document.title = `Count: ${count}`;
}, [count]);
return <div>{count}</div>;
}
渲染时,Fiber 节点的 memoizedState 链表是:
Hook1 (count) → Hook2 (name) → Hook3 (effect) → null
React 通过一个全局指针 currentHook 和 workInProgressHook 来追踪当前处理到链表的哪个位置。每次调用一个 Hook,指针就前进一步。如果在某次渲染中跳过了一个 Hook(比如条件语句导致某个 Hook 没有执行),链表中的节点就会错位——React 会把第二个节点当成第一个来处理,状态完全乱套。这就是"不能在条件语句中调用 Hooks"规则的根本原因。
双 Dispatcher:挂载与更新的分离
React 使用完全不同的函数实现来处理首次渲染(mount)和后续更新(update):
// packages/react-reconciler/src/ReactFiberHooks.js
const HooksDispatcherOnMount = {
useState: mountState,
useEffect: mountEffect,
useRef: mountRef,
useMemo: mountMemo,
useCallback: mountCallback,
// ...
};
const HooksDispatcherOnUpdate = {
useState: updateState,
useEffect: updateEffect,
useRef: updateRef,
useMemo: updateMemo,
useCallback: updateCallback,
// ...
};
// 在 renderWithHooks 中,根据是否首次渲染切换 dispatcher
function renderWithHooks(current, workInProgress, Component, props, ...) {
// current 为 null 说明是首次渲染
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 调用组件函数,过程中所有 useState 等调用都会路由到当前 dispatcher
let children = Component(props, secondArg);
// 渲染完成后,切换到报错 dispatcher,防止在渲染外部调用 Hooks
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
return children;
}
这个设计非常聪明:通过替换 ReactCurrentDispatcher.current 指向的对象,同一个 useState 调用在不同场景下会执行完全不同的代码路径。这是策略模式(Strategy Pattern)的典型应用。
dispatchSetState:触发更新的过程
当你调用 setCount(1) 时,实际执行的是 dispatchSetState:
// packages/react-reconciler/src/ReactFiberHooks.js(简化)
function dispatchSetState(fiber, queue, action) {
const lane = requestUpdateLane(fiber);
const update = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
next: null,
};
// 优化:如果当前没有其他渲染在进行,尝试提前计算新状态
// 如果新状态与当前状态相同,可以直接跳过本次渲染(bail out)
if (isRenderPhaseUpdate(fiber)) {
// 渲染阶段的更新(组件函数内直接调用 setState)
didScheduleRenderPhaseUpdateDuringPass = true;
} else {
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
// Fiber 上没有其他待处理的工作,尝试 eager state 优化
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
try {
const currentState = queue.lastRenderedState;
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
// 新旧状态相同,不触发重新渲染
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
} catch (error) {
// 忽略错误,后续正常渲染时会再次计算
}
}
}
// 将 update 加入 queue 的环形链表
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane);
}
}
}
useEffect 的 Effect 对象
useEffect 的内部实现与 useState 结构类似,但 memoizedState 存储的不是状态值,而是一个 Effect 对象:
// useEffect 的 Effect 对象结构
type Effect = {
tag: HookFlags, // 标识 effect 类型(Passive=useEffect, Layout=useLayoutEffect 等)
create: () => (() => void) | void, // setup 函数
inst: {
destroy: (() => void) | void, // cleanup 函数(setup 的返回值)
tag: HookFlags,
},
deps: Array<mixed> | null, // 依赖数组
next: Effect, // 指向下一个 Effect(形成环形链表)
};
// packages/react-reconciler/src/ReactFiberHooks.js
function mountEffect(create, deps) {
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect, // Fiber flags
HookPassive, // Hook tag
create,
deps,
);
}
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 标记 Fiber 节点需要处理被动副作用
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags, // 首次挂载:一定要执行 effect
create,
createEffectInstance(),
nextDeps,
);
}
function updateEffect(create, deps) {
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const effect = hook.memoizedState;
const inst = effect.inst;
if (currentHook !== null) {
if (nextDeps !== null) {
const prevDeps = currentHook.memoizedState.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 依赖未变化,不标记 HookHasEffect(不会重新执行)
hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps);
return;
}
}
}
// 依赖变化或无依赖数组,标记需要执行
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
inst,
nextDeps,
);
}
HookHasEffect 是关键标志——只有带有这个标志的 Effect,才会在提交阶段真正执行 create 函数。依赖未变化时,Effect 对象依然存在于链表中(保证链表顺序),但不带 HookHasEffect 标志,所以不会执行。
依赖比较:areHookInputsEqual
// packages/react-reconciler/src/ReactFiberHooks.js
function areHookInputsEqual(nextDeps, prevDeps) {
if (prevDeps === null) return false;
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
is 是 Object.is 的封装,使用严格相等加上 -0/NaN 的特殊处理。这意味着对象和数组总是被认为是"不同的"(即使内容相同),因为它们是引用比较——这就是为什么 useEffect 的依赖项中放入对象字面量会导致每次渲染都重新执行 effect。
理解了 Hooks 链表的工作原理,你对 React 的理解就从"知道规则"升华到了"理解规则的必然性"。Hooks 不是魔法,它是对链表数据结构的精心运用,而这个链表的稳定顺序,正是 React 函数组件能够拥有持久化状态的根本保证。
Level 2 · 它是怎么运行的(3-5年经验)
useState 的挂载实现:mountState
// packages/react-reconciler/src/ReactFiberHooks.js
function mountState(initialState) {
// 创建一个新的 Hook 节点,并加入链表末尾
const hook = mountWorkInProgressHook();
// 支持惰性初始化:initialState 可以是函数
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
// 创建更新队列(环形链表)
const queue = {
pending: null, // 待处理的更新(环形链表的头节点)
lanes: NoLanes,
dispatch: null, // dispatch 函数
lastRenderedReducer: basicStateReducer, // useState 使用 basicStateReducer
lastRenderedState: initialState,
};
hook.queue = queue;
// 创建并绑定 dispatch 函数
// dispatch 被绑定到当前 Fiber 和 queue,保证每次调用 setCount 都更新正确的状态
const dispatch = (queue.dispatch = dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
));
return [hook.memoizedState, dispatch];
}
// useState 底层使用的 reducer(只是直接返回新值或调用更新函数)
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
useState 的更新实现:updateState
更新时,React 需要处理可能积压的多个 setState 调用:
// packages/react-reconciler/src/ReactFiberHooks.js
function updateState(initialState) {
// updateReducer 是通用实现,useState 只是用 basicStateReducer 的 useReducer
return updateReducer(basicStateReducer, initialState);
}
function updateReducer(reducer, initialArg, init) {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current = currentHook;
// 处理 baseQueue(上次渲染时因优先级不足被跳过的更新)
let baseQueue = current.baseQueue;
// 合并 pendingQueue 和 baseQueue
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
if (baseQueue !== null) {
// 将 pendingQueue 合并到 baseQueue 末尾
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
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 = removeLanes(update.lane, OffscreenLane);
const isHiddenUpdate = updateLane !== update.lane;
// 检查当前 update 的优先级是否在本次渲染的 lanes 范围内
const shouldSkipUpdate = isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
: !isSubsetOfLanes(renderLanes, updateLane);
if (shouldSkipUpdate) {
// 优先级不足,跳过这个 update,保留到 baseQueue 中
const clone = { ... update };
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
} else {
// 优先级满足,应用这个 update
if (newBaseQueueLast !== null) {
// 如果前面有跳过的 update,这个也要加入 baseQueue 保证顺序
newBaseQueueLast = newBaseQueueLast.next = { ...update, lane: NoLane };
}
// 计算新状态
newState = reducer(newState, update.action);
}
update = update.next;
} while (update !== null && update !== first);
hook.memoizedState = newState;
hook.baseState = newBaseState === null ? newState : newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
const dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
Level 3 · 规范怎么定义的(资深)
本章涉及的 API 规范和设计决策已融入各层级的讨论中。
Level 4 · 边界与陷阱(所有人)
生产环境常见问题
在实际项目中,本章涵盖的概念最常见的问题包括:
-
忽视边界条件:在正常路径下工作正常的代码,在异常路径(网络失败、用户快速操作、组件卸载)下可能产生 bug。始终考虑清理和取消逻辑。
-
过早优化:在没有测量的情况下添加优化代码,增加了复杂度但不一定提升性能。先用 Profiler 确认问题存在,再实施优化。
-
文档与实际行为的差异:React 的行为在不同版本间可能有微妙差异,尤其是并发模式相关的特性。始终以实际测试结果为准。