第 9 章:FunctionComponent 的渲染现场
第 6 章说过,beginWork 按 tag 分派,FunctionComponent 会走到 updateFunctionComponent(ReactFiberBeginWork.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(读更新队列)。两个版本的实现都在 mountState 和 updateState。
这就是为什么你不需要给 useState 传什么"这是第几个 hook",函数每次渲染的调用顺序一致,dispatcher 就能按顺序对上号。顺序一变,就错位。第 12 章(Hooks 总架构)会把这个模式讲全。
第三步:调函数,挂 hooks
dispatcher 就位,函数开跑(599):
let children = Component(props, secondArg);
函数体内那行 useState(0) 执行时,走的是 HooksDispatcherOnMount.useState 也就是 mountState。它干三件事(mountState):
mountWorkInProgressHook()在挂架上追加一个 hook 节点,里面装着memoizedState、queue、next(指向下一个 hook,串成链表)。- 把初始值存进
hook.memoizedState,并建一个更新队列queue。 - 把 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 收放实现的。
动手实验
- 看 dispatcher 切换:在
renderWithHooks(ReactFiberHooks.js:503)的ReactSharedInternals.H = ...那行打断点。首次渲染和更新各打一次,看H分别指向HooksDispatcherOnMount还是OnUpdate。 - 看 hook 挂上去:在
mountWorkInProgressHook(ReactFiberHooks.js:1896)打断点,跑一个用了三个useState/useEffect的组件,看 hook 节点是怎么一个个next串成链的。 - 试出 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 这些非普通组件怎么处理)。