跳到主要内容

第 9 章:FunctionComponent 的渲染现场

第 6 章说过,beginWork 按 tag 分派,FunctionComponent 会走到 updateFunctionComponentReactFiberBeginWork.js:1423)。它里面真正的重头戏,是调用 <Src file="react-reconciler/src/ReactFiberHooks.js" line={503} label="renderWithHooks" />,把你的组件函数跑起来。

就拿第 4 章的 Counter:

function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}

每次渲染,renderWithHooks 都对你的函数做三件事:清空"挂架"、换上正确的 dispatcher、调函数。函数里那行 useState,执行时就是你想象不到的暗流涌动。

先记住这一句:Hooks 之所以叫 Hooks,是因为它们挂在 fiber 上renderWithHooks 先给 fiber 准备好挂架(memoizedState),再让函数跑,函数里每个 useXxx 都变成"往挂架上挂一个 hook"。

一图流:renderWithHooks 三步走

第一步:清空挂架

renderWithHooks 开头把 fiber 的旧状态清掉(527):

workInProgress.memoizedState = null; // hook 链表的挂载点,重新挂
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;

memoizedState 就是"挂架"。这一渲染函数还没跑,先把它清空,等函数跑完,再按顺序把 hook 一个个挂上来。

第二步:换上正确的 dispatcher

接着按"首次还是更新"换实现(561):

ReactSharedInternals.H =
current === null || current.memoizedState === null
? HooksDispatcherOnMount // 首次:useState 去 mountState
: HooksDispatcherOnUpdate; // 更新:useState 去 updateState

dispatcher 是本章最妙的设计。你从 react 里 import 的 useState,内部其实只做一件事:去 ReactSharedInternals.H 上取对应的实现。同一个 useState 符号,首次渲染拿到 mountState(存初始状态),更新时拿到 updateState(读更新队列)。两个版本的实现都在 mountStateupdateState

这就是为什么你不需要给 useState 传什么"这是第几个 hook",函数每次渲染的调用顺序一致,dispatcher 就能按顺序对上号。顺序一变,就错位。第 12 章(Hooks 总架构)会把这个模式讲全。

第三步:调函数,挂 hooks

dispatcher 就位,函数开跑(599):

let children = Component(props, secondArg);

函数体内那行 useState(0) 执行时,走的是 HooksDispatcherOnMount.useState 也就是 mountState。它干三件事(mountState):

  1. mountWorkInProgressHook() 在挂架上追加一个 hook 节点,里面装着 memoizedStatequeuenext(指向下一个 hook,串成链表)。
  2. 把初始值存进 hook.memoizedState,并建一个更新队列 queue
  3. 把 dispatch 函数绑定好
const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
return [hook.memoizedState, dispatch];

注意第 3 步:你拿到的 setCount,是一个**已经绑定好"要改哪个 fiber、往哪个队列塞"**的函数。所以你在事件里调 setCount,React 能立刻知道"这是 Counter 那个 fiber 的 state"。第 21 章(一次 setState 的旅程)会从 dispatchSetState 出发把整个链路走一遍。

收尾:渲染外禁止用 hook

函数返回 children 后,finishRenderingHooks 做最后一件重要的事(657):

ReactSharedInternals.H = ContextOnlyDispatcher;

把 dispatcher 换成"只能用于 Context"的兜底版。这之后如果你在事件回调、setTimeout、普通函数里调用 useState,就会走到这个兜底实现,抛出那句著名的 "Invalid hook call"。Hooks 只能在渲染期间用,这条规则就是靠 dispatcher 收放实现的。

动手实验

  1. 看 dispatcher 切换:在 renderWithHooks(ReactFiberHooks.js:503)的 ReactSharedInternals.H = ... 那行打断点。首次渲染和更新各打一次,看 H 分别指向 HooksDispatcherOnMount 还是 OnUpdate
  2. 看 hook 挂上去:在 mountWorkInProgressHook(ReactFiberHooks.js:1896)打断点,跑一个用了三个 useState/useEffect 的组件,看 hook 节点是怎么一个个 next 串成链的。
  3. 试出 Invalid hook call:把 useState 挪到组件外、或放进 onClick 里,看控制台报错和调用栈,体会 ContextOnlyDispatcher 拦截。

小结

  • renderWithHooks 三步:清空挂架、换 dispatcher、调函数
  • dispatcher 模式useState 是个符号,首次走 mountState、更新走 updateState,靠 ReactSharedInternals.H 切换。
  • mountState 把 hook 串上 memoizedState 链表,setCount绑定好 fiber + 队列的函数。
  • 渲染结束换回 ContextOnlyDispatcher,渲染外调 hook 就报错。
  • 下一步:函数跑完拿到 children,交给 reconcileChildren(第 6、7 章)。先看第 10 章特殊组件(Fragment、memo、Context 这些非普通组件怎么处理)。