跳到主要内容

第 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 同值不渲染的秘密

dispatchSetStateInternal3629)里有个漂亮的优化:如果队列当前是空的(fiber.lanes === NoLanes),它当场用 reducer 算一遍新 state3665):

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 的地方是 updateReducer1294)。它拿到 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:baseStatememoizedState

  • memoizedState:这次渲染算出来的最新值,直接进页面。
  • baseState + baseQueue起点和未处理完的更新,留着给"中断重放"用。

为什么需要两份?第 23 章(并发渲染)会看到,一个低优先级渲染可能被打断。打断后重启时,React 不能从"算了半截的 memoizedState"继续,必须从 baseState 重放 baseQueue,才能保证结果一致。所以 baseState 永远记着"从哪开始算",memoizedState 是"这次算到哪"。useOptimistic(第 16 章)会显式利用这个差异。

动手实验

  1. 断点看入队:在 dispatchSetState(ReactFiberHooks.js:3599)打断点,点按钮。观察 update 对象怎么造的,laneaction 是什么。
  2. 验证急切求值setCount(c => c)(永远返回同一个值),在 dispatchSetStateInternalis(eagerState, currentState) 那行打断点。会发现它当场算出相等,直接走了 bailout,组件根本没重渲染。
  3. 看队列环:连续点三次按钮(不渲染中间态),在 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 章。