14|Hook 的实现视角:从 API 到 Fiber Update Queue 的连接点

14|Hook 的实现视角:从 API 到 Fiber Update Queue 的连接点

本栏目是「React 源码剖析」系列:我会以源码为证据、以架构为线索,讲清 React 从运行时到核心算法的关键设计。开源仓库:https://github.com/facebook/react

在第 13 篇我们把 React Server Components(RSC)在仓库里的分层与边界讲清楚了;这一篇我们把镜头拉回"所有人都天天写、但很少人真正理解它怎么落地"的那一层:Hook。

这篇要回答几个经常读到一半就卡住的问题:

  • useState/useReducer 的更新到底是怎么"接到" Fiber 与 lane 调度上的?
  • 为什么 Hook API 看起来极薄:几乎只是 dispatcher.useState(...)?React 把复杂度藏到哪了?
  • "Rules of Hooks" 的强约束(只能顶层调用、顺序不能变)到底如何落到源码里?

本文核心文件:

  • packages/react/src/ReactHooks.js
  • packages/react/src/ReactSharedInternalsClient.js
  • packages/react-reconciler/src/ReactFiberHooks.js
  • packages/react-reconciler/src/ReactFiberConcurrentUpdates.js
  • packages/react-reconciler/src/ReactFiberWorkLoop.js

0) 先立一个"非直觉结论":Hook API 的"薄"不是偷懒,而是为了把语义钉在 render phase

ReactHooks.js 看,Hook 的公共 API 非常薄:

  • useState(initial)
  • useEffect(create, deps)

它们几乎只是:

  • 取出一个 dispatcher
  • 调用 dispatcher.useState/useEffect/...

源码(packages/react/src/ReactHooks.js):

js 复制代码
function resolveDispatcher() {
  const dispatcher = ReactSharedInternals.H;
  if (__DEV__) {
    if (dispatcher === null) {
      console.error('Invalid hook call...');
    }
  }
  return ((dispatcher: any): Dispatcher);
}

export function useState(initialState) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

如果你把"薄"理解成"简单",就会误判:

  • 真正复杂的是:React 必须在 render phase 建立一套"当前正在渲染的 Fiber + 当前 Hook 指针 + 当前 Dispatcher"的运行时上下文
  • 而 Hook API 的职责是:强制所有 Hook 都从这条上下文里取能力,从而保证"只能在 render 中调用""调用顺序稳定"这两条铁律可以被实现与被检测。

换句话说:

  • Hook API 的薄 = 把调用入口做成"唯一通道"。
  • Dispatcher 的厚 = 把"当前阶段/当前 Fiber/当前 Hook 链表/更新队列/调度"全部绑在一起。

1) ReactSharedInternals.H:Hook 调用能成立的前提,是 Renderer 在 render phase 把 Dispatcher 塞进去

ReactSharedInternals 是 Hook 公共 API 与 Reconciler 之间的一根"暗线"。

在 client 版本里(packages/react/src/ReactSharedInternalsClient.js),你能直接看到这几个关键槽位:

js 复制代码
export type SharedStateClient = {
  H: null | Dispatcher, // ReactCurrentDispatcher for Hooks
  A: null | AsyncDispatcher, // ReactCurrentCache for Cache
  T: null | Transition, // ReactCurrentBatchConfig for Transitions
  S: null | onStartTransitionFinish,
  ...
};

这意味着:

  • H 是 Hook 的"事实源"(谁提供 useState/useEffect 的真正实现)。
  • T 是 Transition 上下文(它会影响 requestUpdateLane 的 lane 选择)。

再反过来看 packages/shared/ReactSharedInternals.js

  • 共享对象本身来自 React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE

所以 Hook 公共 API 之所以能"薄",是因为:

  • React 运行时提供了一个共享槽位。
  • Renderer/Reconciler 在 render phase 负责写入正确的 dispatcher。
  • Hook API 永远从槽位读,并且在槽位为空时(非 render)直接报错。

2) renderWithHooks:Dispatcher 是在这里被安装的(而且会在 render 结束后立刻卸载)

文件:packages/react-reconciler/src/ReactFiberHooks.js

renderWithHooks 是 Hook 的"真正入口"。它做的第一件事不是调用组件,而是建立渲染上下文

  • renderLanes = nextRenderLanes
  • currentlyRenderingFiber = workInProgress
  • 清空 workInProgress.memoizedState/updateQueue/lanes

然后最关键的一步:根据 mount/update 选择 dispatcher 并写入 ReactSharedInternals.H

js 复制代码
ReactSharedInternals.H =
  current === null || current.memoizedState === null
    ? HooksDispatcherOnMount
    : HooksDispatcherOnUpdate;

直觉上你可以把它理解成:

  • mount dispatcher:useStatemountState,会创建 hook/queue,并创建稳定 dispatch。
  • update dispatcher:useStateupdateState,会消费队列、计算新 state。

更"硬"的约束在 render 结束时:

finishRenderingHooks 会把 ReactSharedInternals.H 立刻切回 ContextOnlyDispatcher

js 复制代码
ReactSharedInternals.H = ContextOnlyDispatcher;

这一步的意义是:

  • 一旦离开 render phase,再调用 useStateresolveDispatcher() 拿到的就不是 Hook dispatcher。
  • 这就是"Hook 只能在 function component body 中调用"的运行时地基。

3) Hook 不是数组,是链表:fiber.memoizedState 上挂着一条 Hook list

同一个文件里,React 把 Hook 的数据结构写得很直白:

js 复制代码
export type Hook = {
  memoizedState: any,
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: any,
  next: Hook | null,
};

并且紧跟着就是最重要的全局指针:

js 复制代码
let currentlyRenderingFiber: Fiber = (null: any);
let currentHook: Hook | null = null;
let workInProgressHook: Hook | null = null;

这就是"Rules of Hooks"在实现层面的真正含义:

  • 同一次 render 中,Hook 的调用顺序必须稳定,才能让 mountWorkInProgressHook/updateWorkInProgressHook 在链表上前进。
  • 一旦你条件调用 Hook,链表指针就会错位,React 要么读错队列,要么直接抛错。

3.1 mount:每调用一次 Hook,就 append 一个节点

js 复制代码
function mountWorkInProgressHook(): Hook {
  const hook: Hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null };
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

3.2 update:从 current fiber 的 hook 克隆一份到 work-in-progress

updateWorkInProgressHook 的核心行为是:

  • 找到 nextCurrentHook
  • 找到(或复用)nextWorkInProgressHook
  • 如果没有可复用的 wip hook,就 clone:
js 复制代码
const newHook: Hook = {
  memoizedState: currentHook.memoizedState,
  baseState: currentHook.baseState,
  baseQueue: currentHook.baseQueue,
  queue: currentHook.queue,
  next: null,
};

然后把它 append 到 currentlyRenderingFiber.memoizedState 这条链上。

你会注意到:

  • update 时 queue 是"沿用"的(queue: currentHook.queue)。
  • 这就是为什么同一个 useState 在多次 render 之间能保持同一个 queue。

4) useState 的本体:queue 才是 Hook 更新与调度的连接点

还是在 ReactFiberHooks.jsmountStateImpl 会创建 UpdateQueue

js 复制代码
const queue: UpdateQueue<S, BasicStateAction<S>> = {
  pending: null,
  lanes: NoLanes,
  dispatch: null,
  lastRenderedReducer: basicStateReducer,
  lastRenderedState: (initialState: any),
};

然后 mountState 会创建一个稳定的 dispatch

js 复制代码
const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
queue.dispatch = dispatch;

这一行非常关键:

  • dispatch 捕获了 fiber + queue
  • 因而一次 setState(action) 并不是"调用组件函数",而是"向 queue 里追加 update,并把更新从 fiber 传导到 root 调度"。

这就是 Hook 更新链路的第一块拼图。


5) dispatchSetState:Hook 更新如何选择 lane,并接入 Root 调度

dispatchSetState 入口(同文件):

  1. 先用 requestUpdateLane(fiber) 决定 lane。
  2. 构造一个 update 对象(包含 lane/action/next 等)。
  3. 非 render-phase update:走并发更新队列与 root 调度。

关键代码(省略 DEV 分支):

js 复制代码
const lane = requestUpdateLane(fiber);
const didScheduleUpdate = dispatchSetStateInternal(fiber, queue, action, lane);

5.1 lane 从哪里来:requestUpdateLane 会读 Transition 上下文

ReactFiberWorkLoop.js

js 复制代码
export function requestUpdateLane(fiber: Fiber): Lane {
  if (!disableLegacyMode && (fiber.mode & ConcurrentMode) === NoMode) {
    return SyncLane;
  }
  const transition = requestCurrentTransition();
  if (transition !== null) {
    return requestTransitionLane(transition);
  }
  return eventPriorityToLane(resolveUpdatePriority());
}

这让你把三条线连起来:

  • 普通事件更新:优先级来自事件系统,映射到 lane。
  • startTransition 内的更新:通过 ReactSharedInternals.T(Transition 上下文)走 transition lane。
  • legacy 模式:直接走 SyncLane

5.2 "快路径":eager bailout 解释了为什么某些 setState 不会触发 re-render

dispatchSetStateInternal 里有一段很典型的优化:

  • 当 fiber 与 alternate 都没有 pending lanes(NoLanes)时
  • queue.lastRenderedReducer 存在
  • React 会用上一次 render 的 reducer 预计算 eagerState
  • 如果 eagerStatecurrentState 相等:直接 enqueue 一个 "bailout update",但不 schedule render
js 复制代码
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
  enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
  return false;
}

这段代码是理解"为什么 Hook 需要 lastRenderedReducer/lastRenderedState"的钥匙。

5.3 正常路径:enqueueConcurrentHookUpdatescheduleUpdateOnFiber

当需要真正调度时:

js 复制代码
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
  scheduleUpdateOnFiber(root, fiber, lane);
  entangleTransitionUpdate(root, queue, lane);
  return true;
}

这就是 Hook 更新跨过"Hook 私有世界",进入 "Fiber/Root/WorkLoop" 的那个连接点。


6) enqueueConcurrentHookUpdate:为什么 Hook queue 没有 root backpointer?React 怎么补这个洞?

文件:packages/react-reconciler/src/ReactFiberConcurrentUpdates.js

js 复制代码
export function enqueueConcurrentHookUpdate(fiber, queue, update, lane) {
  enqueueUpdate(fiber, queue, update, lane);
  return getRootForUpdatedFiber(fiber);
}

getRootForUpdatedFiber 里有一段非常"工程现实"的注释:

  • 由于 update queue 没有指向 root 的 backpointer,React 只能沿着 fiber.return 一路向上走到 HostRoot
js 复制代码
// update queues do not have a backpointer to the root, the only way ...
// is to walk up the return path.

并且还留了 TODO:

  • Consider adding a root backpointer on the update queue.

这告诉你一个现实取舍:

  • Hook queue 设计成轻量、可复用、可 clone。
  • root 指针会增加内存/写入复杂度。
  • 于是 React 在 schedule 时用"沿 return path 走到 root"的方式补齐。

另外,你也能看到并发更新为什么要先"暂存"到一个 module-level 数组:

  • concurrentQueues 收集在 render 进行中发生的更新
  • render 结束后(或合适时机)再 finishQueueingConcurrentUpdates() 真正把 update 挂进 circular list(queue.pending)并更新 childLanes

这也是并发模式下"更新进队列的时机"与"Fiber 上 lanes 冒泡到 root 的时机"可以分离的原因。


7) scheduleUpdateOnFiber:Hook 更新最终进入 WorkLoop 的"总开关"

文件:packages/react-reconciler/src/ReactFiberWorkLoop.js

scheduleUpdateOnFiber(root, fiber, lane) 做的事情可以压缩成三段:

  1. 把 root 标记为有 pending updatemarkRootUpdated(root, lane)
  2. 处理中断与 interleaved updates(如果当前 root 正在 render,或 render/commit 被 suspend)
  3. 确保 root 被调度ensureRootIsScheduled(root)

关键片段:

js 复制代码
markRootUpdated(root, lane);
...
ensureRootIsScheduled(root);

并且 legacy 同步更新还有一个经典分支:

  • 如果是 SyncLane 且不在 batch 中:直接 flushSyncWorkOnLegacyRootsOnly()

也就是说:

  • Hook 更新不会"自己跑起来"。
  • Hook 更新只负责把 update 挂到队列、把 lane 标记到 fiber/root。
  • 真正把 CPU 时间分配给这次更新的,是 WorkLoop 的 root scheduling 机制。

8) 一图看懂:从 useState 到 root 调度的最短链路

ReactHooks.useState
resolveDispatcher -> ReactSharedInternals.H
HooksDispatcherOnMount/Update.useState
mountState: queue + dispatchSetState.bind(fiber, queue)
dispatch(action)
dispatchSetState
requestUpdateLane (WorkLoop)
dispatchSetStateInternal: create update
enqueueConcurrentHookUpdate (ConcurrentUpdates)
getRootForUpdatedFiber: walk return path
scheduleUpdateOnFiber (WorkLoop)
markRootUpdated + ensureRootIsScheduled


总结:Hook 的"薄"是表象,真正的契约是"render phase 上下文 + queue + lane"

把本文压缩成三句话:

  • Hook 公共 API 之所以薄,是因为 React 把能力集中到 ReactSharedInternals.H 的 Dispatcher 上,并且只在 render phase 安装它。
  • Hook 状态不是"挂在函数上",而是挂在 Fiber 的 memoizedState 链表里;useState 的核心连接点是 queue,而不是返回的 state 值。
  • dispatchSetState 的本质是:选择 lane → 生成 update → 进入并发更新队列 → 找到 root → scheduleUpdateOnFiber 触发 WorkLoop 调度。

下一篇预告

第 15 篇我们把这条链路再拉长一点,做一次真正的"全链路复盘":

  • updateContainer/scheduleUpdateOnFiber 进入 work loop
  • lane 如何影响 getNextLanes 与时间切片策略
  • render/commit 如何把一次更新落到真实 DOM(以及与 SSR/RSC 的拼装关系)
相关推荐
i7i8i9com1 小时前
React 19学习基础-2 新特性
javascript·学习·react.js
军军君011 小时前
Three.js基础功能学习十:渲染器与辅助对象
开发语言·前端·javascript·学习·3d·前端框架·ecmascript
Marshmallowc1 小时前
React useState 数组 push/splice 后页面不刷新?深度解析状态被『蹭』出来的影子更新陷阱
前端·react.js·前端框架
GIS之路1 小时前
ArcGIS Pro 添加底图的方式
前端·数据库·python·arcgis·信息可视化
Mo_jon1 小时前
vite + vue 快速构建 html 页面 (舒适编写html文件)
前端·vue.js·html
步步为营DotNet2 小时前
深度解析.NET 中Nullable<T>:灵活处理可能为空值的类型
java·前端·.net
rqtz2 小时前
前端相关动画库(GSAP/Lottie/Swiper/AOS)
前端·swiper·lottie·gsap·aos·font-awsome
C_心欲无痕5 小时前
前端如何实现 [记住密码] 功能
前端
qq_3168377511 小时前
uni.chooseMedia 读取base64 或 二进制
开发语言·前端·javascript