第 13 章:useState / useReducer——双缓存与 UpdateQueue
第 9 章说过,你拿到的 setCount 是一个"绑定好 fiber 和队列"的函数。本章追完整条链:setCount(c => c + 1) 从被调用,到新 state 出现在页面上,中间发生了什么。
先立个印象。useState 底层就是 useReducer,两者共用一套更新机制(updateState 内部直接转 updateReducer)。所以本章讲的是 reducer 通用机制,useState 只是把 basicStateReducer 当 reducer 用。
先记住这一句:setState 做的事是把一次"更新"记进 hook 的队列,真正的计算留到 render 阶段。队列是环链表,双缓存(base / memoized)保证中断后能重算。
一图流:setCount 的一生
入队:dispatchSetState
setCount 的实现是 dispatchSetState。它先 requestUpdateLane 要一个 lane(优先级,第 22 章),再造一个 update 对象(3635):
const update = {
lane, // 这条更新的优先级
action, // 你传的东西:值或函数 c => c+1
eagerState: null,// 急切求值的缓存
next: null, // 指向下一条 update,串成环
};
update 是环链表的节点:queue.pending 指向环上最新一条,pending.next 一路转回。一条条 update 排着队,render 时按顺序消费。
急切求值:setState 同值不渲染的秘密
dispatchSetStateInternal(3629)里有个漂亮的优化:如果队列当前是空的(fiber.lanes === NoLanes),它当场用 reducer 算一遍新 state(3665):
const currentState = queue.lastRenderedState;
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
}
关键在 3672:如果急切算出的新值和当前值相等,直接 bailout,连渲染都不调度。这就是为什么 setState(同一个值) 不触发重渲染。省掉了一次多余的 render。
render 阶段:updateReducer 算新 state
真正算 state 的地方是 updateReducer(1294)。它拿到 hook,先做队列合并(1324):把这次渲染新到的 queue.pending 接进 baseQueue 后面,再把 queue.pending 清空。然后从 baseState 出发,沿着 baseQueue 一条条 update 跑 reducer:
let newState = baseState;
let update = first;
do {
// ... 跳过优先级不够的,跑 reducer
newState = reducer(newState, update.action);
update = update.next;
} while (update !== null && update !== first);
如果队列是空的,走快路径(1354):memoizedState = baseState,原样返回。
双缓存:base 和 memoized 为什么是两份
hook 里有两份 state:baseState 和 memoizedState。
memoizedState:这次渲染算出来的最新值,直接进页面。baseState+baseQueue:起点和未处理完的更新,留着给"中断重放"用。
为什么需要两份?第 23 章(并发渲染)会看到,一个低优先级渲染可能被打断。打断后重启时,React 不能从"算了半截的 memoizedState"继续,必须从 baseState 重放 baseQueue,才能保证结果一致。所以 baseState 永远记着"从哪开始算",memoizedState 是"这次算到哪"。useOptimistic(第 16 章)会显式利用这个差异。
动手实验
- 断点看入队:在
dispatchSetState(ReactFiberHooks.js:3599)打断点,点按钮。观察 update 对象怎么造的,lane和action是什么。 - 验证急切求值:
setCount(c => c)(永远返回同一个值),在dispatchSetStateInternal的is(eagerState, currentState)那行打断点。会发现它当场算出相等,直接走了 bailout,组件根本没重渲染。 - 看队列环:连续点三次按钮(不渲染中间态),在
updateReducer(ReactFiberHooks.js:1294)打断点,看baseQueue上串着几条 update。
小结
- useState 就是 useReducer,共用一套 update 环链表 + reducer 机制。
setState造 update 塞进queue.pending,计算留到 render。- 急切求值:队列空时当场算,同值直接 bailout 不渲染。
- render 时
updateReducer合并 base + pending,从 baseState 逐个跑 reducer。 - 双缓存:baseState 是"从哪算起",memoizedState 是"算到哪",中断重放靠 base。
- 下一步:useEffect 的 effect 链表和执行时机,第 14 章。