React 19.3.1 源码深度剖析-第 7 章 Hooks 原理全解(React 函数组件的灵魂)

0 阅读57分钟

React 19.3.1 源码深度剖析-第 7 章 Hooks 原理全解(React 函数组件的灵魂)

源码版本:React 19.3.1 本章主文件:

  • packages/react/src/ReactHooks.js(公共 API 的转发层)
  • packages/react-reconciler/src/ReactFiberHooks.js(Hooks 引擎,约 5265 行)
  • packages/react-reconciler/src/ReactFiberNewContext.js(useContext 的底层 readContext)

阅读建议:本章所有结论都落到真实源码行号,建议边读边在编辑器里对照打开上述文件。ReactFiberHooks.js 是本章绝对核心,所有"魔法"都在这里。


目录

  1. 动机与背景:Hooks 为什么出现,又为什么长成这样
  2. 整体心智模型:一句话 + 一张总览图
  3. 核心数据结构:Hook / Update / UpdateQueue / Effect / 更新队列
  4. Dispatcher 机制:全局指针 ReactSharedInternals.H 的精妙切换
  5. 链表存储与 mount/updateWorkInProgressHook:为什么是链表、为什么不能乱序
  6. renderWithHooks 与 finishRenderingHooks:渲染前的准备与渲染后的校验
  7. 为什么"不能在条件/循环里调用 Hook"——完整证据链
  8. useState 与 useReducer:状态的本质
  9. 状态更新队列:环形链表、复用与丢弃的 lane 语义、eager 优化
  10. 渲染阶段更新(render-phase update)与最多 25 次重渲染
  11. useEffect:effect 的环形链表、三种标记、异步 flush 与 deps 比较
  12. useRef / useMemo / useCallback:稳定性与短路
  13. useContext:不占 hook 链表位置的订阅机制
  14. use:统一读取 Context 与 Promise 的新入口
  15. React 19 新 Hook:useOptimistic / useActionState / useDeferredValue / useTransition
  16. 与其他模块的交互
  17. 边界场景与坑(穷举)
  18. 性能影响与量化
  19. 用法与最佳实践(正例 + 反例)
  20. 源码索引表
  21. 自测 FAQ(20 问)
  22. 本章要点小结

1. 动机与背景:Hooks 为什么出现,又为什么长成这样

1.1 类组件的痛点

在 React 16.8 之前,所有"有状态"或"有副作用"的逻辑只能写在 class 组件里。class 组件有几类根深蒂固的问题:

  • 状态逻辑难以复用:mixins 被废弃后,复用逻辑只能靠 render props 或高阶组件(HOC)。这两种方式都会把组件树"套娃"式地包裹起来,造成"wrapper hell",同时让 DevTools 里出现大量无意义的包裹节点。
  • 复杂组件变得难以理解:一个 componentDidMount 里往往同时做了数据订阅、事件监听、定时器设置;componentWillUnmount 里又要把它们一一清理。相关的逻辑被拆散到不同生命周期,不相关的逻辑又被迫挤在同一个生命周期里。
  • this 指向与类语法的门槛:类字段、bind、this 让新手困惑;而 this 在事件回调里丢失又是经典 bug。
  • 难以被编译器优化:class 组件的生命周期耦合了"何时调用",静态分析难以预测副作用边界。

1.2 Hooks 的设计目标

Hooks 的核心主张是:把"状态"和"副作用"从组件实例(this)上剥离,变成一组可以任意组合、可以局部调用的函数。同一个组件里可以调用多次 useState、useEffect,每次调用都是独立的状态单元。

但这就带来一个根本性的工程难题:函数组件每次渲染都会重新执行,函数内部的局部变量每次都是全新的。那状态、effect、ref 这些"需要跨渲染存活的东西"保存在哪里?

答案不是保存在函数里,而是保存在 React 内部的 fiber(纤程)节点上。每个 fiber 通过一条单向链表串起它所有 hook 的状态。这正是 ReactFiberHooks.js 存在的根本原因。

1.3 为什么用"链表"而不是"数组"

这是一个反复被问到的问题。数组([state0, state1, ...])看起来更简单,但 React 选择了单向链表。原因有二:

  1. 调用顺序的稳定性依赖"位置"而非"索引":链表天然按"调用顺序"组织,每个 hook 就是链表上的一个节点,渲染时从头顺序遍历。这和"Hooks 必须在顶层、按固定顺序调用"的规则严丝合缝。
  2. 增量创建、不预分配:数组需要预先知道长度,链表可以在渲染过程中"走到哪建到哪"。配合 mountWorkInProgressHook(新建节点)和 updateWorkInProgressHook(克隆节点),React 能在渲染进行中动态地把 hook 挂到 fiber 上。

本章会反复回到"链表 = 调用顺序"这个核心不变量。它是理解 Hooks 一切行为的钥匙。


2. 整体心智模型:一句话 + 一张总览图

一句话:每个函数组件在渲染时,React 会把它体内按调用顺序产生的每个 Hook 状态,以单向链表的形式挂在 workInProgress.memoizedState 上;并由一个全局 dispatcher(ReactSharedInternals.H)把 useState/useEffect 等公共 API 重定向到"挂载版/更新版/重渲染版"的具体实现。

flowchart TB
    subgraph API["公共 API 层 (ReactHooks.js)"]
        U1["useState / useEffect / useRef ..."]
    end
    subgraph DISP["Dispatcher 层 (全局指针 ReactSharedInternals.H)"]
        D0[&#34;ContextOnlyDispatcher<br/>(渲染外: 全抛错)&#34;]
        D1[&#34;HooksDispatcherOnMount<br/>(首渲染)&#34;]
        D2[&#34;HooksDispatcherOnUpdate<br/>(更新)&#34;]
        D3[&#34;HooksDispatcherOnRerender<br/>(重渲染)&#34;]
    end
    subgraph ENGINE[&#34;引擎层 (ReactFiberHooks.js)&#34;]
        RW[&#34;renderWithHooks&#34;]
        M[&#34;mountWorkInProgressHook /<br/>updateWorkInProgressHook&#34;]
        FR[&#34;finishRenderingHooks&#34;]
    end
    subgraph FIBER[&#34;Fiber 节点&#34;]
        F[&#34;workInProgress.memoizedState → Hook 链表<br/>workInProgress.updateQueue → Effect 环形链表&#34;]
    end

    U1 --> D0
    U1 --> D1
    U1 --> D2
    U1 --> D3
    D1 --> RW
    D2 --> RW
    D3 --> RW
    RW --> M
    M --> F
    RW --> FR

这张图把全章的骨架立起来了。后面的每一章小节,都是在展开其中某一个方框的内部细节。


3. 核心数据结构:逐字段解释

3.1 Hook 类型(ReactFiberHooks.js:197)

// packages/react-reconciler/src/ReactFiberHooks.js:197
export type Hook = {
  memoizedState: any,   // 当前渲染"已稳定"的状态(对 useState 就是 state;对 useEffect 是 Effect 对象;对 useRef 是 {current})
  baseState: any,       // 基础状态:所有被"本次渲染 lane 之外优先级"跳过的更新,最终收敛到的状态
  baseQueue: Update<any, any> | null, // 被跳过的更新形成的"延迟队列"
  queue: any,           // 该 hook 的更新队列(UpdateQueue),dispatch 存于此
  next: Hook | null,    // 链表指针,指向同一个组件里的下一个 hook
};

逐字段说明:

  • memoizedState:这个词在 React 源码里出现极多,字面意思是"被记忆(memoize)的状态"。对不同的 hook 它含义不同:
    • useState / useReducer:就是当前的 state。
    • useEffect / useLayoutEffect / useInsertionEffect:存的是 Effect 对象(见 3.4)。
    • useRef:存的是 {current: initialValue} 这个 ref 对象本身。
    • useMemo / useCallback:存的是 [value, deps] 或 [callback, deps] 这个二元数组。
    • useDeferredValue:存的是被延迟的值。 所以不要把它简单理解成"state",它是"该 hook 想要跨渲染保留的主值"。
  • baseState:状态计算的"起点"。在 updateReducerImpl 里,更新被逐个执行,只有那些"本次渲染优先级足够"的更新才会真正改变 memoizedState;优先级不够被跳过的更新,会被克隆进 baseQueue,并把它们执行前的状态作为新的 baseState。这是"并发渲染时低优先级更新不丢失"的基础(详见第 9 章)。
  • baseQueue:被跳过的更新构成的环形链表首节点(的下一个,因为环形链表用 next 回指自身)。它需要被保留到下一次更高优先级的渲染里重新处理。
  • queue:UpdateQueue 对象(见 3.3),持有 dispatch(稳定引用)、pending(环形待处理更新)、lastRenderedReducer、lastRenderedState(用于 eager 优化)。
  • next:把同一个组件体内的多个 hook 串成单向链表。

3.2 Update 与 UpdateQueue(ReactFiberHooks.js:167 / :177)

// packages/react-reconciler/src/ReactFiberHooks.js:167
export type Update<S, A> = {
  lane: Lane,                 // 本次更新所属车道(优先级)
  revertLane: Lane,           // 乐观更新的"回滚车道";非乐观更新为 NoLane
  action: A,                  // 动作:要么是值(useState),要么是函数(useReducer)
  hasEagerState: boolean,     // 是否已在 dispatch 时提前算出 state
  eagerState: S | null,       // 提前算出的 state
  next: Update<S, A>,         // 环形链表指针
  gesture: null | ScheduledGesture, // 仅在 enableGestureTransition 下使用
};

// packages/react-reconciler/src/ReactFiberHooks.js:177
export type UpdateQueue<S, A> = {
  pending: Update<S, A> | null,      // 环形链表的"最后一个"更新(next 回指首节点)
  lanes: Lanes,                      // 所有被纠缠的车道(用于 transition 纠缠)
  dispatch: (A => mixed) | null,     // setState / dispatch 的稳定引用
  lastRenderedReducer: ((S, A) => S) | null, // 上次渲染用的 reducer(eager 优化用)
  lastRenderedState: S | null,       // 上次渲染得到的 state(eager 优化比较用)
};

注意 pending 是环形链表:用 last.next = first 的环形结构表示"待处理更新集合"。enqueueRenderPhaseUpdate 和 dispatchSetStateInternal 都靠这个环形结构把多次 setState 串起来。

3.3 Effect 与 EffectInstance(ReactFiberHooks.js:219 / :223)

// packages/react-reconciler/src/ReactFiberHooks.js:219
type EffectInstance = {
  destroy: void | (() => void), // effect 清理函数;卸载或重建时为 undefined
};

// packages/react-reconciler/src/ReactFiberHooks.js:223
export type Effect = {
  tag: HookFlags,                     // 标记:Passive / Layout / Insertion 及 HasEffect 等
  inst: EffectInstance,               // 留存 destroy 的可变容器(类似 RefCell)
  create: () => (() => void) | void,  // 用户的 effect 回调(返回清理函数或不返回)
  deps: Array<mixed> | void | null,   // 依赖数组
  next: Effect,                       // 环形链表指针(挂在 fiber.updateQueue.lastEffect)
};
  • EffectInstance 单独拆出来,是为了让"清理函数 destroy"这个有状态的东西能跨渲染存活而不被不可变的 Effect 结构覆盖。注释里官方直言这是"额外的内存开销,但概念上正确"。
  • Effect 本身也是环形链表,挂在 fiber.updateQueue.lastEffect(见 3.5)。

3.4 FunctionComponentUpdateQueue(ReactFiberHooks.js:249)

// packages/react-reconciler/src/ReactFiberHooks.js:249
export type FunctionComponentUpdateQueue = {
  lastEffect: Effect | null,                   // effect 环形链表的尾节点
  events: Array<EventFunctionPayload<...>> | null, // useEffectEvent 收集的事件
  stores: Array<StoreConsistencyCheck<...>> | null, // useSyncExternalStore 一致性检查
  memoCache: MemoCache | null,                 // useMemoCache(React 编译器用)
};

这个对象挂在 fiber.updateQueue(注意:和 class 组件用的是同名不同结构的 updateQueue)。它承载了函数组件所有"副作用型"数据:effect 环形链表、event 函数、外部 store 检查、memo 缓存。

3.5 存储模型的全局视图

flowchart LR
    F[&#34;Fiber(workInProgress)&#34;]
    F -->|memoizedState| H1[&#34;Hook#0<br/>memoizedState/baseState/queue/next&#34;]
    H1 -->|next| H2[&#34;Hook#1&#34;]
    H2 -->|next| H3[&#34;Hook#2 ... 单向链表&#34;]
    F -->|updateQueue| UQ[&#34;FunctionComponentUpdateQueue&#34;]
    UQ -->|lastEffect| E1[&#34;Effect(环形)<br/>next 回指首节点&#34;]
    H1 -->|queue| Q[&#34;UpdateQueue<br/>pending(环形)/dispatch&#34;]

两个独立链表:

  • 一个是 fiber.memoizedState 上的 Hook 链表(状态型 hook 串这里);
  • 另一个是 fiber.updateQueue.lastEffect 上的 Effect 环形链表(副作用型 hook 串这里)。

useContext 两者都不占——它走 fiber.dependencies.firstContext(见第 13 章)。


4. Dispatcher 机制:全局指针 ReactSharedInternals.H 的精妙切换

4.1 公共 API 只是转发器

打开 packages/react/src/ReactHooks.js,你会发现 useState、useEffect、useRef 等函数体高度雷同:

// packages/react/src/ReactHooks.js:66
export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

每个公共 API 都做同一件事:调用 resolveDispatcher() 拿到当前 dispatcher,然后把调用转给它。

resolveDispatcher(ReactHooks.js:24):

// packages/react/src/ReactHooks.js:24
function resolveDispatcher() {
  const dispatcher = ReactSharedInternals.H;
  if (__DEV__) {
    if (dispatcher === null) {
      console.error(
        'Invalid hook call. Hooks can only be called inside of the body of a function component...'
      );
    }
  }
  // 故意不自己抛错,因为这条路径是"热路径",且有助于内联
  return dispatcher as any as Dispatcher;
}

关键点:

  1. 所有 hook 的真正实现都不在 react 包,而在 react-reconciler 包的 HooksDispatcherOnXxx 里。公共包只是个"取指针 + 转发"的壳。这就是为什么 react 和 react-dom(或 react-reconciler)版本必须匹配——ReactSharedInternals.H 由 renderer 写入,如果两边版本错配,指针为 null,就触发上面那段 "Invalid hook call"。
  2. ReactSharedInternals.H 是全局单例指针。它决定了当组件体内调用 useState 时,实际执行的是 mountState 还是 updateState 还是 rerenderState。

4.2 四个 dispatcher(ReactFiberHooks.js:3898 起)

源码里定义了四个生产环境 dispatcher(DEV 还有各自带校验的变体):

// packages/react-reconciler/src/ReactFiberHooks.js:3898
export const ContextOnlyDispatcher: Dispatcher = {
  readContext,
  use,
  useCallback: throwInvalidHookError,
  useContext: throwInvalidHookError,
  useEffect: throwInvalidHookError,
  // ... 其余 hook 全部 throwInvalidHookError
};

// :3926
const HooksDispatcherOnMount: Dispatcher = {
  readContext,
  use,
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  // ... 全部指向 mountXxx
  useState: mountState,
  useReducer: mountReducer,
  useRef: mountRef,
  // ...
};

// :3954
const HooksDispatcherOnUpdate: Dispatcher = {
  // ... 全部指向 updateXxx
  useState: updateState,
  useReducer: updateReducer,
  useRef: updateRef,
  useEffect: updateEffect,
  // ...
};

// :3982
const HooksDispatcherOnRerender: Dispatcher = {
  // ... 全部指向 updateXxx 或 rerenderXxx
  useState: rerenderState,
  useReducer: rerenderReducer,
  useEffect: updateEffect,
};

它们的分工:

Dispatcher触发时机行为特征
ContextOnlyDispatcher任何渲染阶段之外(组件函数体之外)除了 use / readContext,其余全部抛错
HooksDispatcherOnMount组件的首次渲染每个 hook 都"从零建立":新建链表节点、新建队列
HooksDispatcherOnUpdate组件因 props/state/context 变化而重新渲染每个 hook "克隆并复用"上一次的节点
HooksDispatcherOnRerender因渲染阶段更新触发的额外重渲染(renderWithHooksAgain)状态型 hook 重新消费更新队列,副作用型复用 update 逻辑

4.3 为什么要有"渲染外抛错"的 dispatcher

ContextOnlyDispatcher 的存在是一个防御性设计。设想如果用户把 useState 写在了 if 里、或写在了事件回调里、或写在了普通函数里:这些地方不在 React 渲染组件函数体的上下文中,于是 ReactSharedInternals.H 仍然是 ContextOnlyDispatcher,调用 useState 就会命中 throwInvalidHookError(见 ReactFiberHooks.js:445):

// packages/react-reconciler/src/ReactFiberHooks.js:445
function throwInvalidHookError() {
  throw new Error(
    'Invalid hook call. Hooks can only be called inside of the body of a function component...'
  );
}

注意 react 包里的 resolveDispatcher 在 DEV 下只是 console.error(不抛错,因为热路径),而真正的"抛错"发生在 ContextOnlyDispatcher 的 throwInvalidHookError。两者配合,既在热路径上保持轻量,又在违例时给出清晰错误。

4.4 dispatcher 切换的精妙之处

切换逻辑集中在 renderWithHooks(ReactFiberHooks.js:563)生产分支:

// packages/react-reconciler/src/ReactFiberHooks.js:563
ReactSharedInternals.H =
  current === null || current.memoizedState === null
    ? HooksDispatcherOnMount
    : HooksDispatcherOnUpdate;

这里用一个非常巧妙(也有已知局限)的判据区分 mount / update:current 为 null(首次挂载),或者 current.memoizedState 为 null(上一次也没用任何"有状态"的 hook),就当作 mount;否则是 update。

为什么用 memoizedState === null 而不是"是否挂载"来判定的注释里也写清楚了(:546):

Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used. Non-stateful hooks (e.g. context) don't get added to memoizedState, so memoizedState would be null during updates and mounts.

也就是说:如果一个组件只用 useContext(不写进 memoizedState),那么它的 current.memoizedState 在更新时也可能为 null,于是会被误判成 mount。这正是 DEV 下需要 HooksDispatcherOnMountWithHookTypesInDEV 这个"带类型校验的挂载 dispatcher"来兜底的原因(:552),它能在没有有状态 hook 的更新场景里,仍用 mount 行为但附加顺序校验。

切换的收尾在 finishRenderingHooks(:660):

// packages/react-reconciler/src/ReactFiberHooks.js:660
ReactSharedInternals.H = ContextOnlyDispatcher;

渲染一结束,立刻把指针拨回 ContextOnlyDispatcher。这样组件函数体之外的任何 hook 调用都会立刻抛错——既防止误用,也保证下一次渲染开始时指针是干净的(虽然 renderWithHooks 开头会重新赋值,但保持"用后即废"是良好实践)。


5. 链表存储与 mount/updateWorkInProgressHook

5.1 模块级光标(ReactFiberHooks.js:264)

// packages/react-reconciler/src/ReactFiberHooks.js:264
let currentlyRenderingFiber: Fiber = null as any;
let currentHook: Hook | null = null;       // 指向"当前"hook 链表游标(用于 update 克隆)
let workInProgressHook: Hook | null = null; // 指向"进行中"hook 链表游标(正在构建的 wip 链表)

这三个不是 fiber 的字段,而是 模块级(文件级)的全局变量。React 同一时刻只渲染一个组件,所以用全局游标是安全的,且避免了把游标塞进每个 fiber 的开销。它们由 renderWithHooks 在渲染前设置(currentlyRenderingFiber = workInProgress),由 finishRenderingHooks 在渲染后复位为 null。

5.2 mountWorkInProgressHook(ReactFiberHooks.js:983)

首次渲染时,每个 hook 都通过它创建新节点并挂到链表:

// packages/react-reconciler/src/ReactFiberHooks.js:983
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

  if (workInProgressHook === null) {
    // 这是链表里的第一个 hook
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // 追加到链表末尾
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

逻辑很直白:若当前 wip 链表为空,就让 fiber.memoizedState 指向这个 hook,同时 workInProgressHook 也指向它;否则把它接到 workInProgressHook.next,并前移游标。这就是链表"边渲染边生长"的过程。

5.3 updateWorkInProgressHook(ReactFiberHooks.js:1004)

更新渲染时,hook 不能新建,必须从 current(上一次已提交的 fiber)对应的 hook 克隆过来:

// packages/react-reconciler/src/ReactFiberHooks.js:1004
function updateWorkInProgressHook(): Hook {
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    // 已经有进行中的(来自上一次重渲染尝试),直接复用
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
    // 从当前 hook 克隆
    if (nextCurrentHook === null) {
      const currentFiber = currentlyRenderingFiber.alternate;
      if (currentFiber === null) {
        throw new Error(
          'Update hook called on initial render. This is likely a bug in React. Please file an issue.'
        );
      } else {
        throw new Error('Rendered more hooks than during the previous render.');
      }
    }

    currentHook = nextCurrentHook;

    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null,
    };

    if (workInProgressHook === null) {
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

这个函数藏着"为什么乱序会报错"的关键:

  1. 它严格依赖 currentHook.next 来定位"下一个应该从 current 克隆的 hook"。如果本次渲染的 hook 数量或顺序与上次不同,游标就会错位。
  2. 当链表已经走到末尾(nextCurrentHook === null)却还有 hook 要调用,就抛 "Rendered more hooks than during the previous render."(渲染了比上次更多的 hook)。
  3. 反过来,如果本次 hook 比上次少,游标停在中途,会在 finishRenderingHooks 里被 currentHook.next !== null 捕获,抛 "Rendered fewer hooks than expected. This may be caused by an accidental early return statement."(渲染的 hook 比预期少,可能是提前 return 了)。

这套机制就是"Rules of Hooks"能在运行时被强制执行的底层保障。详情见第 7 章。


6. renderWithHooks 与 finishRenderingHooks

6.1 renderWithHooks:渲染入口(ReactFiberHooks.js:505)

这是函数组件被协调器(beginWork)调用时的真正入口。它做四件事:

  1. 设置全局上下文(renderLanes、currentlyRenderingFiber,:513、:514)。
  2. 选择 dispatcher(ReactFiberHooks.js:563,见 4.4)。
  3. 执行组件函数体(Component(props, secondArg),ReactFiberHooks.js:599),期间所有 useState 等调用会通过 ReactSharedInternals.H 落到 mountXxx/updateXxx。
  4. 处理渲染阶段更新与 StrictMode 双调用,最后调用 finishRenderingHooks。

一段值得精读的逻辑是 StrictMode 双调用(ReactFiberHooks.js:595):

// packages/react-reconciler/src/ReactFiberHooks.js:595
const shouldDoubleRenderDEV =
  __DEV__ && (workInProgress.mode & StrictLegacyMode) !== NoMode;

shouldDoubleInvokeUserFnsInHooksDEV = shouldDoubleRenderDEV;
let children = __DEV__
  ? callComponentInDEV(Component, props, secondArg)
  : Component(props, secondArg);
shouldDoubleInvokeUserFnsInHooksDEV = false;

// 检查是否有渲染阶段更新
if (didScheduleRenderPhaseUpdateDuringThisPass) {
  children = renderWithHooksAgain(workInProgress, Component, props, secondArg);
}

if (shouldDoubleRenderDEV) {
  // 开发模式下组件函数被调用两次,用于检测副作用
  setIsStrictModeForDevtools(true);
  try {
    children = renderWithHooksAgain(workInProgress, Component, props, secondArg);
  } finally {
    setIsStrictModeForDevtools(false);
  }
}

注意一个精妙细节:StrictMode 下,用户函数(如 init 初始化器、useMemo 的工厂)在第一次调用组件时被双调用,第二次不再双调用(shouldDoubleInvokeUserFnsInHooksDEV 只在第一次为 true)。官方注释解释:这是因为 use 在 suspend 时会复用第一次传入的 promise,而 useMemo 的 deps 在第二次会命中,从而复用第一次的结果——必须保证"用户函数的 reactive 输入来自同一次组件调用"。

6.2 finishRenderingHooks:渲染收尾与校验(ReactFiberHooks.js:636)

// packages/react-reconciler/src/ReactFiberHooks.js:636
function finishRenderingHooks(current, workInProgress, Component): void {
  // ... DEV 下保存 _debugHookTypes、thenableState ...

  // 关键:渲染结束,指针拨回 ContextOnlyDispatcher
  ReactSharedInternals.H = ContextOnlyDispatcher; // :660

  // 校验:是否渲染了过少的 hook
  const didRenderTooFewHooks =
    currentHook !== null && currentHook.next !== null; // :664

  renderLanes = NoLanes;
  currentlyRenderingFiber = null as any; // :668
  currentHook = null;        // :670
  workInProgressHook = null; // :671

  // ... DEV 下复位 hookTypesDev 等 ...

  didScheduleRenderPhaseUpdate = false; // :700

紧接其后的 :704 是真正的校验抛错:

// packages/react-reconciler/src/ReactFiberHooks.js:704(位于 if (didRenderTooFewHooks) 内)
throw new Error(
  'Rendered fewer hooks than expected. This may be caused by an accidental ' +
    'early return statement.'
);

而"渲染了更多 hook"的报错在 updateWorkInProgressHook 的 :1048(见 5.3)。两者合起来,在运行时强制了"hook 数量与顺序在每次渲染中必须一致"。

此外,finishRenderingHooks 末尾还有一个上下文变化检查(:714):

// packages/react-reconciler/src/ReactFiberHooks.js:714
if (current !== null) {
  if (!checkIfWorkInProgressReceivedUpdate()) {
    const currentDependencies = current.dependencies;
    if (
      currentDependencies !== null &&
      checkIfContextChanged(currentDependencies)
    ) {
      markWorkInProgressReceivedUpdate();
    }
  }
}

这段解决了一个微妙问题:如果组件本次渲染没有任何 props/state 变化,但某处 useContext 读取的 context 值变了,那么即使"看起来没更新",也要标记 WorkInProgressReceivedUpdate,以确保 effect 等能正确重新评估。这是 context 与 hook 联动的收口。

6.3 renderWithHooksAgain:重渲染循环(ReactFiberHooks.js:791)

当渲染阶段发生了 setState(render-phase update),组件需要"就地再渲染一次"来消费这些更新。它也是一个 dispatcher 切换点:

// packages/react-reconciler/src/ReactFiberHooks.js:791(节选)
function renderWithHooksAgain(workInProgress, Component, props, secondArg): any {
  // 把指针切到 "re-render" dispatcher
  ReactSharedInternals.H = ...HooksDispatcherOnRerender...;
  // 然后再次调用组件函数体
  ...
}

重渲染用的是 HooksDispatcherOnRerender(:3982),它的 useState→rerenderState、useReducer→rerenderReducer,这些实现会重新消费 hook.queue 里的更新,而不是像 updateState 那样再从 current 克隆。这也是为什么渲染阶段更新能"立刻看到结果"。


7. 为什么"不能在条件/循环里调用 Hook"——完整证据链

这是 Hooks 最著名的规则,但很多文章只说"因为顺序"。本章给你从源码层面完整闭环的证据链。

7.1 规则的技术本质

Hook 的状态不保存在函数闭包里,而是保存在 fiber.memoizedState 上的单向链表,且每个 hook 对应链表上的第 N 个节点(N = 调用次序)。渲染时,更新路径通过 updateWorkInProgressHook(:1004)按顺序从 current 克隆节点:

currentHook = nextCurrentHook;              // 取上一个 hook 的 next
const newHook = { ...currentHook ... };     // 克隆
workInProgressHook = workInProgressHook.next = newHook; // 接到 wip 链表

这个克隆过程完全依赖"每个 hook 调用都对应 current 链表上的同位置节点"。一旦调用顺序变了,克隆到的就是"错误的那个旧状态"。

7.2 三条报错路径(完整证据)

把代码里所有相关报错点串起来,就得到一条铁证:

证据 A(渲染了更多 hook):updateWorkInProgressHook :1048

} else {
  throw new Error('Rendered more hooks than during the previous render.');
}

触发条件:nextCurrentHook === null(current 链表已走到末尾),但组件还想再调一个 hook。典型场景:本次渲染比上次多了一个 useState(比如新增了一个没被条件保护的 hook,或上次因为 if(false) 少调了一个)。

证据 B(渲染了更少 hook):finishRenderingHooks :664 + :704

const didRenderTooFewHooks =
  currentHook !== null && currentHook.next !== null; // :664
// ... 之后:
if (didRenderTooFewHooks) {
  throw new Error('Rendered fewer hooks than expected... early return...'); // :704
}

触发条件:渲染结束时 currentHook 不是链表的最后一个(说明还有"本应被调用"的 hook 没被调用)。典型场景:if (condition) return; 提前退出,导致后面的 useEffect 等没执行。

证据 C(渲染外调用):ContextOnlyDispatcher :3898 + throwInvalidHookError :445

export const ContextOnlyDispatcher: Dispatcher = {
  useCallback: throwInvalidHookError,
  useEffect: throwInvalidHookError,
  useReducer: throwInvalidHookError,
  useRef: throwInvalidHookError,
  useState: throwInvalidHookError,
  // ...
};

触发条件:在组件函数体之外(事件回调、普通函数、setTimeout 等)调用 hook,此时 ReactSharedInternals.H 是 ContextOnlyDispatcher,直接抛 "Invalid hook call"。

证据 D(DEV 顺序表校验):updateHookTypesDev :323 + warnOnHookMismatchInDev :351

// packages/react-reconciler/src/ReactFiberHooks.js:323
function updateHookTypesDev(): void {
  if (__DEV__) {
    const hookName = currentHookNameInDev as any as HookType;
    if (hookTypesDev !== null) {
      hookTypesDev.push(hookName); // 首次:记录顺序
      hookTypesUpdateIndexDev++;
      if (hookTypesDev[hookTypesUpdateIndexDev] !== hookName) {
        warnOnHookMismatchInDev(hookName); // 后续:比对顺序
      }
    }
  }
}

DEV 下,React 在首次渲染记录每个 hook 的"类型顺序"(hookTypesDev),之后每次渲染逐个比对。即使数量恰好相同,只要类型顺序变了(比如把 useState 和 useEffect 换了位置),也会在控制台打出一张"上一轮 vs 这一轮"的对照表并警告。这是数量之外的第二道防线。

7.3 用图说话

flowchart TD
    A[&#34;组件函数体执行&#34;]
    A --> B{&#34;是否在渲染上下文中?<br/>(H === ContextOnlyDispatcher?)&#34;}
    B -- 否 --> C[&#34;证据C: Invalid hook call 抛错&#34;]
    B -- 是 --> D[&#34;按调用顺序逐个调 mountXxx/updateXxx&#34;]
    D --> E{&#34;本次调用第 N 个 hook&#34;}
    E --> F[&#34;update: 克隆 current 链表第 N 个节点&#34;]
    F --> G{&#34;克隆时 current 链表<br/>已到末尾?&#34;}
    G -- 是 --> H[&#34;证据A: Rendered more hooks 抛错&#34;]
    G -- 否 --> I[&#34;继续&#34;]
    I --> J{&#34;渲染结束, currentHook<br/>是否为最后一个?&#34;}
    J -- 否(提前return) --> K[&#34;证据B: Rendered fewer hooks 抛错&#34;]
    J -- 是 --> L[&#34;正常&#34;]
    D -.DEV.-> M[&#34;证据D: 类型顺序表比对, 不一致则警告&#34;]

7.4 那"为什么不报错但行为诡异"的情况

最危险的情况不是抛错,而是数量相同但位置语义错位:例如把 const [a] = useState(0) 放在 if (x) 里,且 x 在两次渲染间从 true 变 false,导致该 useState 这次没调用。结果:原本"第 2 个 hook"的 useEffect 这次变成了"第 1 个 hook",克隆到了上一次 useState 的状态——类型不匹配,行为完全错乱,但未必抛错(除非 DEV 顺序表救场)。这正是规则"必须在顶层、按固定顺序"的根本原因:它不是 React 的随意限制,而是链表存储模型的必然约束。


8. useState 与 useReducer:状态的本质

8.1 useState 是 useReducer 的特例

这是 React 源码里最优雅的设计之一:useState 的 reducer 就是 basicStateReducer。

// packages/react-reconciler/src/ReactFiberHooks.js:1265
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === 'function' ? action(state) : action;
}
  • 如果传的是函数(setCount(c => c + 1)),就执行它得到新 state;
  • 如果传的是值(setCount(5)),就直接作为新 state。

于是 updateState 完全委托给 updateReducer:

// packages/react-reconciler/src/ReactFiberHooks.js:1962
function updateState<S>(initialState): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, initialState);
}

rerenderState 同理委托 rerenderReducer(:1968)。所以本章后面所有对 useReducer 的剖析,对 useState 100% 成立。

8.2 mountStateImpl 与 mountState(ReactFiberHooks.js:1920 / :1948)

// packages/react-reconciler/src/ReactFiberHooks.js:1920
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    const initialStateInitializer = initialState;
    initialState = initialStateInitializer(); // 懒初始化执行一次
    if (shouldDoubleInvokeUserFnsInHooksDEV) {
      // StrictMode 双调用该初始化器
      setIsStrictModeForDevtools(true);
      try {
        initialStateInitializer();
      } finally {
        setIsStrictModeForDevtools(false);
      }
    }
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState as any,
  };
  hook.queue = queue;
  return hook;
}

// packages/react-reconciler/src/ReactFiberHooks.js:1948
function mountState<S>(initialState): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountStateImpl(initialState);
  const queue = hook.queue;
  const dispatch: Dispatch<BasicStateAction<S>> = dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ) as any;
  queue.dispatch = dispatch;
  return [hook.memoizedState, dispatch];
}

要点:

  1. 懒初始化:若 initialState 是函数,只在 mount 时执行一次(得到初始值),之后更新渲染不会再执行它。这正是 useState(() => expensiveComputation()) 优化生效的原因。
  2. dispatch 是稳定引用:dispatchSetState.bind(null, fiber, queue) 在 mount 时创建一次,之后更新渲染通过 updateState → updateReducer 返回同一个 queue.dispatch(因为 updateWorkInProgressHook 克隆了 queue)。这就是为什么你可以在 useEffect 依赖里放心省略 setState 而不触发无限循环——引用永远不变。
  3. lastRenderedReducer / lastRenderedState 是为 eager 优化准备的(见第 9 章)。

8.3 mountReducer(ReactFiberHooks.js:1270)

与 mountStateImpl 几乎一致,只是 reducer 由调用方传入,且没有"懒初始化"分支(初始化函数 init 是可选的第三参数):

// packages/react-reconciler/src/ReactFiberHooks.js:1270(节选)
function mountReducer(reducer, initialArg, init?): [S, Dispatch<A>] {
  const hook = mountWorkInProgressHook();
  let initialState;
  if (init !== undefined) {
    initialState = init(initialArg);
    // StrictMode 双调用 init
  } else {
    initialState = initialArg as any as S;
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = { pending: null, lanes: NoLanes, dispatch: null,
                  lastRenderedReducer: reducer, lastRenderedState: initialState };
  hook.queue = queue;
  const dispatch = queue.dispatch = dispatchReducerAction.bind(null, currentlyRenderingFiber, queue);
  return [hook.memoizedState, dispatch];
}

注意 dispatchReducerAction 与 dispatchSetState 是不同函数(见 9.1),但本质都是把更新入队。


9. 状态更新队列:环形链表、复用与丢弃的 lane 语义、eager 优化

这是全书最硬核的一段。理解了它,你就理解了 React 并发更新的"复用与丢弃"哲学。

9.1 派发函数 dispatchSetState / dispatchReducerAction

mountState 用的是 dispatchSetState,mountReducer 用的是 dispatchReducerAction。两者都调用同一个核心:dispatchSetStateInternal(ReactFiberHooks.js:3656)。

// packages/react-reconciler/src/ReactFiberHooks.js:3656
function dispatchSetStateInternal<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
  lane: Lane,
): boolean {
  const update: Update<S, A> = {
    lane,
    revertLane: NoLane,
    gesture: null,
    action,
    hasEagerState: false,
    eagerState: null,
    next: null as any,
  };

  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update); // 渲染阶段更新
  } else {
    const alternate = fiber.alternate;
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // 队列当前为空 → 可以尝试 eager 优化
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        // 提前算出 next state
        const currentState = queue.lastRenderedState as any;
        const eagerState = lastRenderedReducer(currentState, action);
        update.hasEagerState = true;
        update.eagerState = eagerState;
        if (is(eagerState, currentState)) {
          // 算出结果和当前 state 相同 → 短路 bailout
          enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
          return false;
        }
      }
    }
    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      scheduleUpdateOnFiber(root, fiber, lane);
      entangleTransitionUpdate(root, queue, lane);
      return true;
    }
  }
  return false;
}

逐段拆解:

9.2 渲染阶段更新(render-phase update)

isRenderPhaseUpdate(ReactFiberHooks.js:3838)判断:当前 fiber 是否正是 currentlyRenderingFiber(或它的 alternate)。

// packages/react-reconciler/src/ReactFiberHooks.js:3838
function isRenderPhaseUpdate(fiber: Fiber): boolean {
  const alternate = fiber.alternate;
  return (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  );
}

如果在渲染阶段调用了 setState,就会走 enqueueRenderPhaseUpdate(ReactFiberHooks.js:3846):

// packages/react-reconciler/src/ReactFiberHooks.js:3846
function enqueueRenderPhaseUpdate(queue, update): void {
  didScheduleRenderPhaseUpdateDuringThisPass =
    didScheduleRenderPhaseUpdate = true;
  const pending = queue.pending;
  if (pending === null) {
    update.next = update; // 自环
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;
}

它把更新塞进同一个 queue.pending 环形链表,并置 didScheduleRenderPhaseUpdate = true。renderWithHooks 检测到这个标志后,会调用 renderWithHooksAgain 再渲染一次,就地消费掉这些更新,于是"在 render 里 setState 能立刻生效"。

⚠️ 虽然能这么做,但 React 官方强烈不推荐在渲染阶段 setState(除非是为了"根据上一次渲染派生状态"这种极少数场景)。因为它会触发额外重渲染,次数过多会被限制在 RE_RENDER_LIMIT = 25(ReactFiberHooks.js:295)。renderWithHooksAgain 内部会累加重渲染次数,超过 25 直接抛错,防止无限循环。

9.3 eager 优化(短路条件)

回到 dispatchSetStateInternal 的这段:

if (fiber.lanes === NoLanes &&
    (alternate === null || alternate.lanes === NoLanes)) {
  // 队列为空
  const lastRenderedReducer = queue.lastRenderedReducer;
  if (lastRenderedReducer !== null) {
    const currentState = queue.lastRenderedState as any;
    const eagerState = lastRenderedReducer(currentState, action);
    update.hasEagerState = true;
    update.eagerState = eagerState;
    if (is(eagerState, currentState)) {
      // 关键短路:算出 state 没变
      enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
      return false; // 不调度重渲染!
    }
  }
}

eager 优化的完整条件(全部满足才触发):

  1. 队列当前为空(fiber.lanes === NoLanes 且 alternate 也无 lanes)——即没有待处理的并发更新;
  2. lastRenderedReducer 存在(useOptimistic 故意设为 null,因此乐观更新不走 eager 优化,见 15.1);
  3. 用上次的 lastRenderedState + 本次 action 提前算出 eagerState;
  4. is(eagerState, currentState) 为 true——即新 state 与当前 state 用 Object.is 比较相等。

满足则调用 enqueueConcurrentHookUpdateAndEagerlyBailout:把更新(带着 hasEagerState=true、eagerState)仍入队(以保证语义正确、可被 future render 复用),但不调用 scheduleUpdateOnFiber,即不触发重渲染,直接 return false。

这就是 setState 传一个"和当前值相等的值"时组件不重渲染的底层原因,也是性能优化点。注意:它比较的是 queue.lastRenderedState(上次渲染后稳定的 state),不是 fiber.memoizedState,两者在并发场景可能不同。

如果更新导致 state 真的变化,则落到普通路径 enqueueConcurrentHookUpdate → scheduleUpdateOnFiber,正常安排一次渲染。

9.4 updateReducerImpl:复用与丢弃的 lane 语义(ReactFiberHooks.js:1316)

这是状态计算的心脏。它展示了 React 在并发渲染中如何"按优先级复用部分更新、丢弃(延迟)另一部分"。

// packages/react-reconciler/src/ReactFiberHooks.js:1316(节选,保留主干)
function updateReducerImpl(hook, current, reducer): [S, Dispatch<A>] {
  const queue = hook.queue;
  if (queue === null) {
    throw new Error(
      'Should have a queue. You are likely calling Hooks conditionally...'
    );
  }
  queue.lastRenderedReducer = reducer;

  let baseQueue = hook.baseQueue;       // 上次被跳过、延迟到本次的更新
  const pendingQueue = queue.pending;   // 本次新进来的更新
  if (pendingQueue !== null) {
    if (baseQueue !== null) {
      // 合并 baseQueue 与 pendingQueue 两个环形链表
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }

  const baseState = hook.baseState;
  if (baseQueue === null) {
    hook.memoizedState = baseState; // 没有待处理更新
  } else {
    const first = baseQueue.next;
    let newState = baseState;
    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;
    do {
      const updateLane = removeLanes(update.lane, OffscreenLane);
      const isHiddenUpdate = updateLane !== update.lane;
      let shouldSkipUpdate = isHiddenUpdate
        ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
        : !isSubsetOfLanes(renderLanes, updateLane);

      if (shouldSkipUpdate) {
        // ★ 复用与丢弃:优先级不足的更新被"克隆"进 newBaseQueue 保留
        const clone = { ...update, lane: updateLane, /* ... */ next: null };
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
        currentlyRenderingFiber.lanes = mergeLanes(currentlyRenderingFiber.lanes, updateLane);
        markSkippedUpdateLanes(updateLane);
      } else {
        // ★ 优先级足够的更新被真正应用
        if (update.hasEagerState) {
          newState = update.eagerState; // 用 eager 阶段算好的 state
        } else {
          newState = reducer(newState, update.action);
        }
      }
      update = update.next;
    } while (update !== null && update !== first);

    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = newBaseQueueFirst; // 闭环
    }
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast; // 保留被跳过的更新到下一次
    queue.lastRenderedState = newState;
  }
  const dispatch = queue.dispatch;
  return [hook.memoizedState, dispatch];
}

这是整章最值得反复咀嚼的算法。我们用一个具体例子理解"复用与丢弃":

例子:优先级混合的连续更新

假设 state 初始为 0,renderLanes 当前只包含 Lane2(中等优先级)。先后发生三个 setState:+1(Lane1,高)、+2(Lane3,低)、+3(Lane2,中)。

  1. 三个更新全部进入 queue.pending 环形链表(按 dispatch 顺序排列)。
  2. 本次渲染的 renderLanes 是 {Lane2}。
  3. 遍历时:
    • +1(Lane1):isSubsetOfLanes({Lane2}, Lane1) 为 false → shouldSkip,克隆进 newBaseQueue,newBaseState = 0(此时 newState 仍为 0)。
    • +2(Lane3):isSubsetOfLanes({Lane2}, Lane3) 为 false → 同样 skip,克隆进 newBaseQueue,newBaseState 仍 0。
    • +3(Lane2):isSubsetOfLanes({Lane2}, Lane2) 为 true → 应用,newState = 0 + 3 = 3。
  4. 遍历结束:
    • hook.memoizedState = 3(本次渲染提交的状态)。
    • hook.baseState = 0(被跳过的更新对应的起点)。
    • hook.baseQueue = 克隆后的 [+1, +2] 环形链表(保留!)。
    • 顺带把 Lane1、Lane3 merge 进 fiber.lanes,让调度器知道"还有更低优先级的活要干"。

于是当 Lane1 的高优先级渲染到来时:

  • baseState = 0,baseQueue = [+1, +2],加上本次新可能的 pending。
  • 这次 renderLanes 包含 Lane1,+1 被应用 → newState = 1,然后若也包含 Lane3 则 +2 应用 → 3,否则 +2 继续被克隆保留。
  • 最终每个更新都在正确的优先级上被精确消费一次,既不会丢失,也不会重复应用——这就是"复用与丢弃"的真正含义:丢弃的不是更新本身,而是在当前优先级下"暂时不应用",但保留到未来合适的渲染中复用。

再看几个关键细节:

  • clone 而非 reuse:被跳过的更新会被 clone 一份(lane: updateLane 等字段拷贝),因为原始的 update 属于上一轮 render 的环形链表,不能原地修改(它可能被别的 fiber/渲染共享引用)。这是不可变更新思想的体现。
  • markSkippedUpdateLanes + mergeLanes:把被跳过的 lane 标记到 fiber 上,调度器据此安排后续渲染。这正是 React 并发"可中断、可恢复"的底层支撑。
  • eagerState 优先:若更新在 dispatch 时已 eager 算出 state(hasEagerState),这里直接取 eagerState,省掉一次 reducer 调用。
  • OffscreenLane 处理:隐藏树(Offscreen)里的更新会被加一个额外 bit,渲染时若不在该隐藏树上下文中就 skip,避免隐藏内容抢占主渲染。
flowchart TD
    subgraph Q[&#34;queue.pending (环形)&#34;]
        P1[&#34;update +1 (Lane1)&#34;]
        P2[&#34;update +2 (Lane3)&#34;]
        P3[&#34;update +3 (Lane2)&#34;]
        P1 --> P2 --> P3 --> P1
    end
    Q --> R[&#34;updateReducerImpl 遍历&#34;]
    R --> C{&#34;isSubset(renderLanes, update.lane)?&#34;}
    C -- 否 --> S[&#34;clone 进 baseQueue 保留<br/>markSkippedUpdateLanes&#34;]
    C -- 是 --> A[&#34;reducer 应用<br/>newState 更新&#34;]
    S --> BK[&#34;hook.baseQueue 留待下次&#34;]
    A --> MS[&#34;hook.memoizedState 提交&#34;]

9.5 dispatchReducerAction 与 transition 纠缠

dispatchSetStateInternal 在普通路径末尾调用 entangleTransitionUpdate(ReactFiberHooks.js:3867):

// packages/react-reconciler/src/ReactFiberHooks.js:3867
function entangleTransitionUpdate(root, queue, lane): void {
  if (isTransitionLane(lane)) {
    let queueLanes = intersectLanes(queueLanes, root.pendingLanes);
    const newQueueLanes = mergeLanes(queueLanes, lane);
    queue.lanes = newQueueLanes;
    markRootEntangled(root, newQueueLanes);
  }
}

如果本次更新是一个 transition lane,就把队列里的所有 transition lane "纠缠(entangle)"在一起:要么一起完成,要么一起被中断重来。这保证了 useTransition 的"要么都提交,要么都回滚"语义。


10. 渲染阶段更新与最多 25 次重渲染

回顾 9.2:enqueueRenderPhaseUpdate 置 didScheduleRenderPhaseUpdate = true。renderWithHooks(:605)检测到后调用 renderWithHooksAgain。

重渲染次数受 RE_RENDER_LIMIT = 25(ReactFiberHooks.js:295)约束。renderWithHooksAgain 内部有一个计数器,每次重渲染自增,超过 25 直接抛错——这是防止"render 里 setState 触发 setState 触发 setState…"无限循环的最后防线。

实战建议:

  • 渲染阶段 setState 的唯一正当用途是"状态依赖于上一次渲染的结果,且无法用派生计算表达"的极少数场景(如 const [derived, setDerived] = useState(...); if (prev !== derived) { setDerived(...); return; })。
  • 任何"为了在计算后修正状态"的 render-phase setState,都应重构为 useMemo 派生或用 useEffect 在提交后处理。

11. useEffect:effect 的环形链表、标记差异、异步 flush 与 deps 比较

11.1 三个 effect 标记的差异

useEffect、useLayoutEffect、useInsertionEffect 在底层都用同一套 mountEffectImpl / updateEffectImpl,只是传入的 fiberFlags(打在 fiber 上)和 hookFlags(打在 Effect.tag 上)不同:

HookfiberFlags(fiber.flags)hookFlags(Effect.tag)执行时机
useEffectPassiveEffect | PassiveStaticEffect(DEV Strict 下再加 MountPassiveDevEffect)HookPassivecommit 之后异步(scheduler 调度 flushPassiveEffects)
useLayoutEffectUpdateEffect | LayoutStaticEffect(DEV 加 MountLayoutDevEffect)HookLayoutcommit 阶段同步、在浏览器绘制前
useInsertionEffectUpdateEffectHookInsertionDOM 变更前、layout 之前(用于注入样式,避免闪烁)

源码对照(ReactFiberHooks.js:2697 起):

// packages/react-reconciler/src/ReactFiberHooks.js:2697
function mountEffect(create, deps): void {
  if (__DEV__ && (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode) {
    mountEffectImpl(
      MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
      HookPassive, create, deps,
    );
  } else {
    mountEffectImpl(PassiveEffect | PassiveStaticEffect, HookPassive, create, deps);
  }
}

function updateEffect(create, deps): void {
  updateEffectImpl(PassiveEffect, HookPassive, create, deps); // :2721
}

function mountInsertionEffect(create, deps): void {
  mountEffectImpl(UpdateEffect, HookInsertion, create, deps); // :2782
}

function mountLayoutEffect(create, deps): void {
  let fiberFlags = UpdateEffect | LayoutStaticEffect;
  if (__DEV__ && (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode) {
    fiberFlags |= MountLayoutDevEffect;
  }
  return mountEffectImpl(fiberFlags, HookLayout, create, deps); // :2796
}

11.2 mountEffectImpl 与 effect 环形链表(ReactFiberHooks.js:2640)

// packages/react-reconciler/src/ReactFiberHooks.js:2640
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.flags |= fiberFlags; // 在 fiber 上打标记
  hook.memoizedState = pushSimpleEffect(
    HookHasEffect | hookFlags, // 首次必定 HasEffect(需要执行)
    createEffectInstance(),
    create,
    nextDeps,
  );
}

pushSimpleEffect(ReactFiberHooks.js:2588 附近)→ pushEffectImpl(ReactFiberHooks.js:2605)把 effect 挂到 fiber.updateQueue.lastEffect 的环形链表:

// packages/react-reconciler/src/ReactFiberHooks.js:2605
function pushEffectImpl(effect): Effect {
  let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = componentUpdateQueue;
  }
  const lastEffect = componentUpdateQueue.lastEffect;
  if (lastEffect === null) {
    componentUpdateQueue.lastEffect = effect.next = effect; // 自环
  } else {
    const firstEffect = lastEffect.next;
    lastEffect.next = effect;
    effect.next = firstEffect;
    componentUpdateQueue.lastEffect = effect; // 尾指针前移
  }
  return effect;
}

注意环形链表用 lastEffect.next 指向"首 effect"。commit 阶段遍历 effect 时,从 lastEffect.next(首节点)开始,顺 next 走到 lastEffect 为止(用 firstEffect !== lastEffect 判断结束),覆盖全部 effect。

flowchart LR
    UQ[&#34;fiber.updateQueue.lastEffect&#34;]
    UQ --> E3[&#34;Effect#3 (last)&#34;]
    E3 --> E1[&#34;Effect#1 (first)&#34;]
    E1 --> E2[&#34;Effect#2&#34;]
    E2 --> E3
    style E3 stroke:#f66

11.3 updateEffectImpl 与 deps 比较(ReactFiberHooks.js:2657)

// packages/react-reconciler/src/ReactFiberHooks.js:2657
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const effect = hook.memoizedState;
  const inst = effect.inst;

  if (currentHook !== null) {
    if (nextDeps !== null) {
      const prevEffect = currentHook.memoizedState;
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // deps 没变 → 跳过执行,只改变 create,不带 HasEffect
        hook.memoizedState = pushSimpleEffect(hookFlags, inst, create, nextDeps);
        return;
      }
    }
  }

  currentlyRenderingFiber.flags |= fiberFlags; // 需要执行
  hook.memoizedState = pushSimpleEffect(
    HookHasEffect | hookFlags, inst, create, nextDeps,
  );
}

逻辑:

  • 如果上一次有 deps,且本次 deps 经 areHookInputsEqual 比较相等,则 effect 的 tag 不带 HookHasEffect,commit 阶段遍历时就会跳过它的执行(但仍更新 create、deps,以便下次比较)。
  • 若 deps 变了(或无 deps——nextDeps === null),则打上 HookHasEffect,commit 阶段会先执行上一次的 destroy,再执行新的 create。

11.4 areHookInputsEqual:deps 比较的陷阱(ReactFiberHooks.js:456)

// packages/react-reconciler/src/ReactFiberHooks.js:456
function areHookInputsEqual(nextDeps, prevDeps): boolean {
  if (__DEV__) {
    if (ignorePreviousDependencies) return false; // 热重载时强制重跑
  }
  if (prevDeps === null) {
    if (__DEV__) console.error('...final argument during this render, but not previous...');
    return false;
  }
  if (__DEV__) {
    if (nextDeps.length !== prevDeps.length) {
      console.error('The final argument passed to %s changed size between renders...');
    }
  }
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false; // 有一项不等 → 认为变了
  }
  return true;
}

关键点:

  1. 比较用的是 is(shared/objectIs,即 Object.is),不是 === 的宽松语义,但和 === 几乎一致,唯一区别是 NaN === NaN 为 false 而 Object.is(NaN, NaN) 为 true,以及 -0/+0。所以 {a:1} 这种每次 render 都是新对象的 deps 永远不等——这是 deps 比较的头号陷阱(见 17 章)。
  2. 长度变化会报警(DEV),但比较循环用 prevDeps.length && nextDeps.length 取较小值,所以长度变化不会 crash,只是行为可能诡异。
  3. 只在 nextDeps !== null 时比较:如果 useEffect(fn) 不传 deps(即 deps === undefined → nextDeps = null),则 updateEffectImpl 里 nextDeps !== null 为 false,每次渲染都重新执行 effect。

11.5 effect 的异步执行(Passive effect 的 flush)

useEffect 打的是 PassiveEffect 标记。commit 阶段(在 ReactFiberWorkLoop.js 的 commitRootImpl 中)会把带 PassiveEffect 的 fiber 收集起来,并在 commit 完成、浏览器有机会绘制之后,通过 scheduler 异步调用 flushPassiveEffects(约 ReactFiberWorkLoop.js:3806)来:① 执行上一次 effect 的 destroy;② 执行本次 effect 的 create。

这就是为什么 useEffect 里的 setState 不会"同步阻塞 commit"、不会引发"同步又触发渲染又 commit"的递归——它被推迟到一个微任务/宏任务里异步进行。而 useLayoutEffect 的 HookLayout 在 commit 的同步阶段、浏览器绘制前执行,因此它能"读 DOM 布局并同步修正",但代价是会阻塞绘制(这也是为什么官方建议优先用 useEffect)。

注:flushPassiveEffects 的具体实现位于 ReactFiberWorkLoop.js(react 19 中约第 3806 行附近),它遍历 rootWithPendingPassiveEffects 收集的 fiber 上的 effect 环形链表,逐个调用 destroy/create。本章聚焦 ReactFiberHooks.js,对 work loop 的调用约定在此说明即可,避免编造行号。


12. useRef / useMemo / useCallback:稳定性与短路

12.1 useRef:永不重建(ReactFiberHooks.js:2628)

// packages/react-reconciler/src/ReactFiberHooks.js:2628
function mountRef<T>(initialValue: T): {current: T} {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}

function updateRef<T>(initialValue: T): {current: T} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState; // 直接复用旧的 ref 对象
}

useRef 的奥秘在于:更新渲染时 updateRef 直接返回 hook.memoizedState,也就是 mount 时创建的那个 {current} 对象,绝不会新建。这意味着:

  • 你每次渲染拿到的 ref 是同一个对象引用,ref.current 的修改会跨渲染保留。
  • 在 StrictMode 双调用下,useRef 也不会被"重置"——它的 memoizedState 在两次组件调用之间被复用,所以 ref.current 的值在双调用之间是连续的(这是 StrictMode 下 ref 双调用陷阱的来源,见 17 章)。
  • 因为引用稳定,useRef 永远不需要 deps,也不会触发 re-render。

12.2 useMemo:deps 命中则短路(ReactFiberHooks.js:2944)

// packages/react-reconciler/src/ReactFiberHooks.js:2944
function mountMemo(nextCreate, deps): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  if (shouldDoubleInvokeUserFnsInHooksDEV) {
    // StrictMode 双调用工厂
    setIsStrictModeForDevtools(true);
    try { nextCreate(); } finally { setIsStrictModeForDevtools(false); }
  }
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

function updateMemo(nextCreate, deps): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (nextDeps !== null) {
    const prevDeps = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      return prevState[0]; // 短路:直接返回旧值,不执行 nextCreate
    }
  }
  const nextValue = nextCreate(); // deps 变了才执行
  // ...StrictMode 双调用
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

要点:

  • useMemo 存的是 [value, deps] 二元组。
  • updateMemo 仅在 nextDeps !== null 且 areHookInputsEqual 为 true 时返回旧 value,否则执行 nextCreate 重新计算。
  • useMemo 不是语义保证:React 官方明确说明,它只是"优化提示",在内存紧张或并发中断时可能丢弃缓存重新计算。所以不要依赖 useMemo 来"记忆"副作用或必须只执行一次的逻辑——那是 useRef 或 useEffect 的职责。
  • StrictMode 下 nextCreate 会被双调用,因此工厂函数必须是纯函数(无副作用),否则会执行两次。

12.3 useCallback:本质是 useMemo 的语法糖(ReactFiberHooks.js:2923)

// packages/react-reconciler/src/ReactFiberHooks.js:2923
function mountCallback(callback, deps): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

function updateCallback(callback, deps): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (nextDeps !== null) {
    const prevDeps = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      return prevState[0]; // 返回旧的函数引用
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

useCallback(fn, deps) 等价于 useMemo(() => fn, deps):它缓存的是函数引用本身。目的不是"性能"(函数创建几乎零成本),而是让函数引用在 deps 不变时保持稳定,从而不会因"父组件重渲染导致内联函数引用变化"而破坏子组件的 React.memo 优化或 useEffect 的 deps 比较。


13. useContext:不占 hook 链表位置的订阅机制

13.1 它根本不在 hook 链表里

ReactHooks.js:53 的 useContext 转发到 dispatcher.useContext,而生产 dispatcher(ReactFiberHooks.js:3931)里 useContext: readContext——是的,useContext 直接就是 readContext,完全不走 mountWorkInProgressHook。

13.2 readContext / readContextForConsumer(ReactFiberNewContext.js:553 / :580)

// packages/react-reconciler/src/ReactFiberNewContext.js:553
export function readContext<T>(context: ReactContext<T>): T {
  if (__DEV__) {
    if (isDisallowedContextReadInDEV) {
      console.error('Context can only be read while React is rendering...');
    }
  }
  return readContextForConsumer(currentlyRenderingFiber, context);
}

// packages/react-reconciler/src/ReactFiberNewContext.js:580
function readContextForConsumer(consumer, context): T {
  const value = isPrimaryRenderer
    ? context._currentValue
    : context._currentValue2;

  const contextItem = { context, memoizedValue: value, next: null };

  if (lastContextDependency === null) {
    if (consumer === null) {
      throw new Error('Context can only be read while React is rendering...');
    }
    lastContextDependency = contextItem;
    consumer.dependencies = {
      lanes: NoLanes,
      firstContext: contextItem,
      // _debugThenableState ...
    };
    consumer.flags |= NeedsPropagation;
  } else {
    lastContextDependency = lastContextDependency.next = contextItem;
  }
  return value;
}

机制拆解:

  1. 读取当前值:context._currentValue(主渲染器)。Provider 更新 value 时就是改写这个字段。
  2. 登记订阅:每次 useContext 都会创建一个 contextItem,通过模块级游标 lastContextDependency 串成链表,挂到 consumer.dependencies.firstContext。
  3. NeedsPropagation 标记:在 fiber 上打标记,使 context 变化能向上/向下传播。

13.3 为什么这很重要

  • 不占 hook 链表位置:所以你可以条件式地 useContext(虽然仍建议顶层),它不会破坏 Rules of Hooks 的"数量一致性"——因为它根本不通过 mount/updateWorkInProgressHook。这也是为什么 updateReducerImpl 之外,context 的"是否更新"检查要单独在 finishRenderingHooks(:714)里做。
  • Provider value 变化触发重渲染:当 Provider 的 value 变化,所有消费该 context 且 Object.is 比较不等价的 fiber 都会被标记为需要更新。React 在 propagateContextChange 时,沿 fiber.dependencies.firstContext 链表找到受影响的消费者并安排重渲染。
  • 多个 useContext 同一 context 只订阅一次:因为 readContextForConsumer 按"调用次数"挂 item,但 Provider 变化时是对整个 context 的所有消费者统一比较。

注意:React 19 推荐用 use(Context) 替代 useContext(见第 14 章),因为 use 还能读 promise 并触发 Suspense。


14. use:统一读取 Context 与 Promise 的新入口

14.1 use 的实现(ReactFiberHooks.js:1159)

// packages/react-reconciler/src/ReactFiberHooks.js:1159
function use<T>(usable: Usable<T>): T {
  if (usable !== null && typeof usable === 'object') {
    if (typeof usable.then === 'function') {
      // thenable → 读 Promise,可 suspend
      const thenable = usable;
      return useThenable(thenable);
    } else if (usable.$$typeof === REACT_RECOVERABLE_TYPE) {
      return undefined; // Fiber 是最终渲染器,无需 recover
    } else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
      const context = usable;
      return readContext(context); // 读 Context
    }
  }
  throw new Error('An unsupported type was passed to use(): ' + String(usable));
}

use 是 React 19 的统一读取入口,能处理三种输入:

  1. thenable(Promise-like):调用 useThenable,其内部用 trackUsedThenable(ReactFiberThenable.js)把 promise 登记到 thenableState,并在 promise 未决议时抛出 Suspense 异常,从而挂起组件,等 promise 完成后再用同一 promise(被 memo 化)重渲染。
  2. REACT_RECOVERABLE_TYPE:服务器恢复用的特殊类型,客户端直接返回 undefined。
  3. REACT_CONTEXT_TYPE:等价于 readContext(context),即 useContext 的能力。

14.2 useThenable 与 dispatcher 切换(ReactFiberHooks.js:1098)

// packages/react-reconciler/src/ReactFiberHooks.js:1098(节选)
function useThenable(thenable): T {
  const index = thenableIndexCounter;
  thenableIndexCounter += 1;
  if (thenableState === null) {
    thenableState = createThenableState();
  }
  const result = trackUsedThenable(thenableState, thenable, index,
    __DEV__ ? currentlyRenderingFiber : null);

  // 当 use 导致 suspend,重放组件时用 "re-render" dispatcher;
  // 但如果 use 之后还有其它 hook,需要切回 mount/update dispatcher
  const workInProgressFiber = currentlyRenderingFiber;
  const nextWorkInProgressHook =
    workInProgressHook === null
      ? workInProgressFiber.memoizedState
      : workInProgressHook.next;
  if (nextWorkInProgressHook !== null) {
    // 还有 hook 未处理
  } else {
    // 没有剩余 hook → 切回正常的 mount/update dispatcher
  }
  return result;
}

use(promise) 的核心价值:在条件/循环里也能安全地读 promise 并 suspend——因为 use 不像 useState 那样依赖"在 hook 链表固定位置",它把 thenable 登记在 thenableState(挂 fiber.dependencies._debugThenableState),并通过 thenableIndexCounter 在重渲染时按位置复用同一个 promise。这正是 use 能突破 Rules of Hooks 限制(可在条件里调用)的原因——它的状态不存于 hook 链表,而存于 thenableState。

14.3 use 的约束

  • 不能在 try/catch 里用 use(DEV 会警告,见 finishRenderingHooks :734):因为 suspense 靠抛异常工作,try/catch 会吞掉它,导致悬挂失效。
  • 不能在渲染外调用:use 也经由 resolveDispatcher,渲染外 H 是 ContextOnlyDispatcher,但 ContextOnlyDispatcher 里 use 是有效实现(:3901),所以 use 在渲染外的行为由 usable 类型决定——实际生产中仍应在渲染期调用。

15. React 19 新 Hook:useOptimistic / useActionState / useDeferredValue / useTransition

15.1 useOptimistic:每帧重置的乐观状态(ReactFiberHooks.js:1974 / :2013)

// packages/react-reconciler/src/ReactFiberHooks.js:1974
function mountOptimistic(passthrough, reducer): [S, (A) => void] {
  const hook = mountWorkInProgressHook();
  hook.memoizedState = hook.baseState = passthrough;
  const queue = {
    pending: null, lanes: NoLanes, dispatch: null,
    lastRenderedReducer: null,   // ★ 故意 null:不走 eager 优化
    lastRenderedState: null,
  };
  hook.queue = queue;
  const dispatch = dispatchOptimisticSetState.bind(null, currentlyRenderingFiber, true, queue);
  queue.dispatch = dispatch;
  return [passthrough, dispatch];
}

更新时(updateOptimisticImpl,:2013):

// packages/react-reconciler/src/ReactFiberHooks.js:2013
function updateOptimisticImpl(hook, current, passthrough, reducer): [S, (A) => void] {
  // 关键:每次渲染都把 baseState 重置为 passthrough
  hook.baseState = passthrough;

  const resolvedReducer = typeof reducer === 'function' ? reducer : basicStateReducer;
  return updateReducerImpl(hook, currentHook, resolvedReducer);
}

useOptimistic 的精妙:

  1. baseState 每帧重置为 passthrough:passthrough 是最新的"真实"状态(来自 props/上一次提交)。所有乐观更新都被叠加在它之上。一旦底层状态变化,下一次渲染 passthrough 更新,乐观叠加自动"对齐"到新真实值。
  2. revertLane 实现自动回滚:dispatchOptimisticSetState(ReactFiberHooks.js:3728)创建的更新带 revertLane: requestTransitionLane(transition)。在 updateReducerImpl 里,当 revertLane 对应的 transition 已完成(isSubsetOfLanes(renderLanes, revertLane) 为 true)时,该乐观更新被 continue 跳过(:1482)——即"关联 transition 结束后,乐观更新自动失效回滚到真实状态"。
  3. lastRenderedReducer: null:故意不设 reducer,因此不走 9.3 的 eager 优化(乐观更新不该被"提前 bailout")。
  4. 必须在 transition/action 内调用:dispatchOptimisticSetState 在 DEV 下若检测到 transition === null 且无 pending async action,会警告 "An optimistic state update occurred outside a transition or action"。
sequenceDiagram
    participant U as 用户事件(action)
    participant O as useOptimistic dispatch
    participant Q as UpdateQueue
    participant R as updateReducerImpl
    U->>O: setOptimistic(next)
    O->>Q: 入队 update(revertLane=transitionLane)
    Note over R: 渲染时 baseState=passthrough(真实值)
    R->>R: 叠加乐观更新 → 显示乐观值
    Note over R: transition 完成, revertLane 命中
    R->>R: 跳过乐观更新 → 自动回滚到 passthrough

15.2 useActionState:useState + 乐观 pending + 串行 action 队列(ReactFiberHooks.js:2380)

useActionState(action, initialState) 返回 [state, dispatch, isPending],它的底层由三个 hook 组合而成(这是"hook 组合"思想的绝佳范例):

// packages/react-reconciler/src/ReactFiberHooks.js:2380(节选)
function mountActionState(action, initialStateProp, permalink?): [S, (P) => void, boolean] {
  // Hook 1:状态 hook(用 dispatchSetState,state 存于 thenable,由 use 算法解包)
  const stateHook = mountWorkInProgressHook();
  stateHook.memoizedState = stateHook.baseState = initialState;
  const stateQueue = { pending: null, lanes: NoLanes, dispatch: null,
                       lastRenderedReducer: actionStateReducer,
                       lastRenderedState: initialState };
  stateHook.queue = stateQueue;
  const setState = dispatchSetState.bind(null, currentlyRenderingFiber, stateQueue);
  stateQueue.dispatch = setState;

  // Hook 2:pending 状态 hook(用 dispatchOptimisticSetState,类似 transition pending)
  const pendingStateHook = mountStateImpl(false);
  const setPendingState = dispatchOptimisticSetState.bind(null, currentlyRenderingFiber, false, pendingStateHook.queue);

  // Hook 3:action 队列 hook(串行执行 action)
  const actionQueueHook = mountWorkInProgressHook();
  const actionQueue = { state: initialState, dispatch: null, action, pending: null };
  actionQueueHook.queue = actionQueue;
  const dispatch = dispatchActionState.bind(null, currentlyRenderingFiber, actionQueue, setPendingState, setState);
  actionQueue.dispatch = dispatch;

  actionQueueHook.memoizedState = action; // 记下 action 函数,用于检测变化
  return [initialState, dispatch, false];
}

工作机制:

  • 串行执行:dispatchActionState(ReactFiberHooks.js:2106)把 action payload 入队到 actionQueue.pending(环形链表)。若当前没有正在运行的 action(last === null),立即 runActionStateAction;否则追加到队列尾部,等前一个完成。这保证了"多个 action 顺序执行,且每个都能拿到上一个 action 的结果"——这正是 form action 语义要求的。
  • 乐观 pending:进入 action 前,setPendingState(true) 通过 dispatchOptimisticSetState 乐观地把 isPending 置为 true,并在 action 全部完成后由 revertLane 自动回滚到 false(见 15.1 的回滚机制)。
  • isPending 通过 thenable 解包:stateHook 的 state 实际是一个 thenable(未决议时 suspend),在 use 算法里被 useThenable 解包(见 updateActionState,:2465 起),使得 action 进行中组件能"挂起等待结果"。
  • 异常隔离:若某 action 报错,actionQueue.action 被置 null,后续 action 直接 return(:2118),防止错误级联。

15.3 useDeferredValue:把值延迟到更低优先级(ReactFiberHooks.js:2990 / :3028)

// packages/react-reconciler/src/ReactFiberHooks.js:2990
function mountDeferredValue(value, initialValue?): T {
  const hook = mountWorkInProgressHook();
  return mountDeferredValueImpl(hook, value, initialValue);
}

// packages/react-reconciler/src/ReactFiberHooks.js:3028
function mountDeferredValueImpl(hook, value, initialValue?): T {
  if (initialValue !== undefined && !isRenderingDeferredWork()) {
    // 首次用 initialValue,并调度一个 DeferredLane 切换到最终值
    hook.memoizedState = initialValue;
    const deferredLane = requestDeferredLane();
    currentlyRenderingFiber.lanes = mergeLanes(currentlyRenderingFiber.lanes, deferredLane);
    markSkippedUpdateLanes(deferredLane);
    return initialValue;
  } else {
    hook.memoizedState = value;
    return value;
  }
}

useDeferredValue 的工作方式:

  1. 当提供 initialValue 且当前渲染不是 deferred work 时,先返回 initialValue,同时请求一个 DeferredLane(低优先级车道),把"切换到真实 value"这件事推迟到后台低优先级渲染。这避免了"输入即卡顿"——输入框立刻响应(用旧/初始值渲染),重内容在空闲时再更新。
  2. updateDeferredValueImpl(:3056)在值未变(is)时快速 bailout,变了则在隐藏树/普通树里分别走 mount/update 路径,并通过 isCurrentTreeHidden 判断是否需要隐藏树特殊处理。
  3. 它是 useTransition 的"值版"封装:本质都是把非紧急更新放到低优先级车道。

15.4 useTransition:把回调包进 transition(ReactFiberHooks.js:3425 / :3117)

// packages/react-reconciler/src/ReactFiberHooks.js:3425
function mountTransition(): [boolean, (callback, options?) => void] {
  const stateHook = mountStateImpl(false); // 内部用 useState 存 isPending
  const start = startTransition.bind(null, currentlyRenderingFiber, stateHook.queue, true, false);
  const hook = mountWorkInProgressHook();
  hook.memoizedState = start; // start 函数引用稳定
  return [false, start];
}

mountTransition 内部就是:①一个 useState(false) 表示 isPending;②把 startTransition 绑定到该 state 的 queue。startTransition(ReactFiberHooks.js:3117)做的关键事:

  1. 提升当前更新优先级到 ContinuousEventPriority(setCurrentUpdatePriority);
  2. 设置 ReactSharedInternals.T = currentTransition(标记进入 transition 上下文);
  3. 调 dispatchOptimisticSetState(..., pendingState=true) 乐观地把 isPending 设为 true(带 revertLane,transition 结束后自动回滚到 false)——这就是 isPending 在 transition 进行中显示 true、结束后自动变 false 的来源;
  4. 执行用户 callback,把其中的 setState 都标记为 transition lane(低优先级,可中断);
  5. transition 关联的异步 action scope 完成时,回滚 isPending。

注意:updateTransition / rerenderTransition(:3443 / :3458)第 445 行 useThenable(booleanOrThenable) 说明 isPending 实际可能是一个 thenable(当 action 是异步时),由 use 算法解包为 boolean。这与 15.2 的 useActionState 共享同一套"isPending via thenable"机制。


16. 与其他模块的交互

Hooks 引擎不是孤立的,它的协作方包括:

模块文件与 Hooks 的交互
协调器(beginWork)ReactFiberBeginWork.js函数组件进入 renderWithHooks 的起点;mountIndeterminateComponent 首次渲染后再决定是函数组件还是 class
工作循环ReactFiberWorkLoop.jsscheduleUpdateOnFiber、getWorkInProgressRootRenderLanes、flushPassiveEffects(异步 flush effect)
车道系统ReactFiberLane.jsrenderLanes、isSubsetOfLanes、mergeLanes、removeLanes 等决定更新复用/丢弃
并发更新ReactFiberConcurrentUpdates.jsenqueueConcurrentHookUpdate、enqueueConcurrentHookUpdateAndEagerlyBailout 把更新挂到 fiber 的 update 队列并 entanglement
上下文ReactFiberNewContext.jsreadContext、checkIfContextChanged 供 useContext 与 finishRenderingHooks 使用
共享内部shared/ReactSharedInternals.jsH(dispatcher 指针)、T(transition 上下文)、A(cache dispatcher)
thenableReactFiberThenable.jstrackUsedThenable、createThenableState 供 use 的 Suspense 能力
异步 actionReactFiberAsyncAction.jspeekEntangledActionLane、chainThenableValue 供 action state 与 transition 纠缠
编译器缓存ReactFiberCacheComponent.jsuseMemoCache 的 createCache、memo cache 数据结构

数据流一句话总结:React 入口 → 协调器 → renderWithHooks(切 dispatcher)→ 各 mount/updateXxx(建/克隆 hook 链表 + 登记 effect/context/thenable)→ finishRenderingHooks(校验 + 复位指针)→ 提交阶段 work loop 消费 effect 环形链表 / 响应状态更新重新进入协调器。


17. 边界场景与坑(穷举)

17.1 条件式 hook 报错(运行时)

见第 7 章完整证据链。三种报错:more hooks(updateWorkInProgressHook:1048)、fewer hooks(finishRenderingHooks:704)、渲染外(ContextOnlyDispatcher:3898)。唯一正解:所有 hook 永远在组件顶层、无条件、无循环地调用。

17.2 deps 比较陷阱

  • 每次 render 都是新对象/数组:useEffect(fn, [{}]) 或 [arr.map(...)] 这种内联表达式,每次都是新引用,Object.is 永远不等,effect 每次都跑。正确做法是把依赖"计算到基本类型"或 useMemo 化。
  • 忘记依赖导致 stale closure:useEffect(() => { fetchData(id) }, []) 但用了 id,lint 会警告,因为闭包捕获的是首次渲染的 id。
  • 函数依赖:useEffect(fn, [doThing]) 而 doThing 没被 useCallback 包裹,每次都是新函数,effect 每次重跑。用 useCallback 稳定它。
  • useEffectEvent 解围:React 19 的 useEffectEvent(ReactFiberHooks.js:2728)能把"每次都不一样但又不想作为依赖"的函数标记为 event,避免上述陷阱。

17.3 渲染阶段 setState

  • 会触发 renderWithHooksAgain 就地重渲染(renderWithHooks:605)。
  • 超过 RE_RENDER_LIMIT = 25(ReactFiberHooks.js:295)抛错。
  • 不要在事件处理里依赖它,它只应用于"派生状态"极端场景。

17.4 StrictMode 下 ref 的双调用陷阱

useRef 的 memoizedState 在两次组件调用之间被复用(见 12.1)。在 StrictMode(DEV)下组件函数被调用两次,但 ref.current 是同一个对象。后果:

function Buggy() {
  const ref = useRef(0);
  ref.current++; // 双调用下会 +2,而不是 +1!
  // ...
}

因为 useRef 不重建,ref.current++ 在双调用里执行了两次。同理,在 useEffect 里做"订阅/动画"时 StrictMode 会故意挂载→卸载→再挂载来检测清理不彻底的 bug。正解:副作用的初始化必须可重入,清理函数要完整撤销副作用。

17.5 useState 的函数式更新 vs 值更新

  • setCount(5) → basicStateReducer 直接返回 5;
  • setCount(c => c + 1) → 返回 reducer(state, action) 即 c + 1。
  • 并发陷阱:若依赖"上一次 state"但用值形式(setCount(count + 1)),在多次连续更新被批处理/重渲染时,count 可能是过时的闭包值。永远用函数式更新来读最新 state。

17.6 setState 传相同值的 eager bailout

如 9.3 所述,传与 lastRenderedState 相等的值会触发 eager bailout,不重渲染。如果你"依赖 setState 一定触发 effect"就会踩坑。注意比较基准是 queue.lastRenderedState(上次渲染稳定值),不是当前 render 的临时值。

17.7 useMemo 不是保证

不要依赖 useMemo 做一次性副作用缓存(见 12.2)。需要"只算一次且绝对保留"用 useRef + useState 懒初始化。

17.8 useContext 的"白嫖"与 Provider 值

  • useContext 不占 hook 链表,条件调用不破规则;但仍建议在顶层。
  • Provider 的 value 若每次 render 都是新对象(<Ctx.Provider value={{a:1}}>),会导致所有消费者 re-render(即使内容没变)。用 useMemo 稳定 value,或用 useReducer/useState 持有。

17.9 use 不能在 try/catch

DEV 下 finishRenderingHooks(:734)会检查 checkIfUseWrappedInTryCatch() 并警告。use(promise) 靠抛 Suspense 异常工作,try/catch 会破坏它。

17.10 useOptimistic 必须配 transition/action

单独在普通事件里调 setOptimistic 会 DEV 警告(:3764),因为 revertLane 依赖 transition 上下文来自动回滚。

17.11 渲染中读取 document/window

这不是源码强制,但是惯例:useLayoutEffect 在浏览器环境同步执行,可用;而 useEffect 异步,期间若依赖布局需注意时序。SSR 时 useLayoutEffect 会警告(服务端无 layout),用 useEffect 或 useInsertionEffect 替代。

17.12 多个 state 更新的批处理

React 18+ 默认自动批处理(automatic batching)。一次事件里的多个 setState 合并为一次渲染。dispatchSetStateInternal 的 eager 优化与 enqueueConcurrentHookUpdate 都服务于批处理与优先级。


18. 性能影响与量化

18.1 时间复杂度

  • mountXxx:每个 hook 是 O(1) 的链表节点创建。
  • updateWorkInProgressHook:O(1) 克隆(不遍历整条链表,只前移游标)。
  • updateReducerImpl:O(U),U = baseQueue + pending 的更新数量。最坏情况(大量未决并发更新)与更新数线性相关。
  • areHookInputsEqual:O(D),D = deps 数组长度。

18.2 空间复杂度

  • 每个状态和副作用 hook 各占一个 Hook / Effect 对象 + UpdateQueue / EffectInstance。
  • useMemo/useCallback 额外存 [value, deps] 二元组。
  • useContext 不增加 hook 对象(只增加一个 contextItem 挂 dependencies)。
  • useMemoCache(编译器)在 updateQueue.memoCache 存二维数组;enableNoCloningMemoCache 开启时跨 render 共享同一实例(:1221),节省拷贝。

18.3 瓶颈与规避

  • 长链表遍历:一个组件若有几百个 hook,每次渲染都要走一遍链表。虽是 O(1) 每节点,但绝对量大时仍可观。规避:拆分小组件,减少单组件 hook 数。
  • effect 环形链表过大:大量 useEffect 会让 commit 阶段的 effect 遍历变长。规避:合并相关 effect,用 useEffectEvent 减少依赖敏感。
  • 不必要的 re-render:useState 值更新触发 re-render 是常态。eager bailout(9.3)已优化"值未变"场景;还可用 React.memo + 稳定 useCallback 减少子组件重渲染。
  • useContext 广播:一个高频变化的 context 会让所有消费者重渲染。规避:拆分 context、用 useSyncExternalStore 做细粒度订阅、或用 selector 模式。
  • deps 比较开销:超大 deps 数组每次渲染都比较。规避:把依赖收敛为基本类型,或用 useEffectEvent 移除不稳定函数依赖。

19. 用法与最佳实践(正例 + 反例)

19.1 状态派生优先于状态拷贝

// 反例:render 里 setState 修正(触发重渲染,最多 25 次)
const [list, setList] = useState(items);
const [filtered, setFiltered] = useState(items);
useEffect(() => { setFiltered(list.filter(f)); }, [list]);

// 正例:useMemo 派生,无额外 state
const [list] = useState(items);
const filtered = useMemo(() => list.filter(f), [list, f]);

19.2 函数式更新读最新 state

// 反例:依赖闭包 count(并发下可能过期)
const onClick = () => setCount(count + 1);

// 正例:函数式
const onClick = () => setCount(c => c + 1);

19.3 稳定回调传给子组件

// 反例:每次 render 新函数,破坏 React.memo
<Child onDo={payload => doSomething(payload)} />

// 正例:useCallback 稳定 + 依赖收敛
const onDo = useCallback(payload => doSomething(payload), [doSomething]);
<Child onDo={onDo} />

19.4 effect 清理完整

useEffect(() => {
  const id = setInterval(tick, 1000);
  return () => clearInterval(id); // 必须清理,否则 StrictMode 双挂载会叠加定时器
}, []);

19.5 用 use 替代条件式 context 读取场景

React 19 起,读 promise 用 use(promise) 触发 Suspense;读 context 也可用 use(Ctx)(语义等同 useContext)。注意别在 try/catch 里用 use。

19.6 乐观更新用 useOptimistic + action

const [optState, addOptimistic] = useOptimistic(state, (cur, next) => [...cur, next]);
// 在 transition/action 内:
addOptimistic(newItem); // 立即显示,底层 state 更新后自动对齐/回滚

19.7 不要把 useMemo 当缓存保证

若需"只计算一次且绝不丢弃",用 useState 懒初始化或 useRef:

const [computed] = useState(() => expensiveOnce()); // 只算一次

20. 源码索引表

主题文件符号 / 行号
公共 API 转发packages/react/src/ReactHooks.jsresolveDispatcher:24、useState:66、useReducer:73、useRef:82、useEffect:87、useMemo:143、useContext:53、use:207、useOptimistic:226、useActionState:234
Hook 类型ReactFiberHooks.jsHook:197、Effect:223、EffectInstance:219、FunctionComponentUpdateQueue:249
Update / UpdateQueueReactFiberHooks.jsUpdate:167、UpdateQueue:177
模块级光标ReactFiberHooks.jscurrentlyRenderingFiber:264、currentHook:270、workInProgressHook:271
渲染阶段更新标志ReactFiberHooks.jsdidScheduleRenderPhaseUpdate:277、RE_RENDER_LIMIT:295
DEV 顺序校验ReactFiberHooks.jshookTypesDev:303、updateHookTypesDev:323、warnOnHookMismatchInDev:351
deps 比较ReactFiberHooks.jsareHookInputsEqual:456
渲染入口ReactFiberHooks.jsrenderWithHooks:505、dispatcher 选择 :563、StrictMode 双渲染 :595、重渲染触发 :605
渲染收尾校验ReactFiberHooks.jsfinishRenderingHooks:636、复位指针 :660、过少 hook :664/:704、context 变化 :714
重播/重渲染ReactFiberHooks.jsreplaySuspendedComponentWithHooks:755、renderWithHooksAgain:791
链表节点ReactFiberHooks.jsmountWorkInProgressHook:983、updateWorkInProgressHook:1004、过多 hook 抛错 :1048
use / useThenableReactFiberHooks.jsuse:1159、useThenable:1098、useMemoCache:1181
reducer/stateReactFiberHooks.jsbasicStateReducer:1265、mountReducer:1270、updateReducerImpl:1316、updateReducer:1307、updateState:1962、rerenderState:1968
mountStateReactFiberHooks.jsmountStateImpl:1920、mountState:1948
乐观/actionReactFiberHooks.jsmountOptimistic:1974、updateOptimisticImpl:2013、ActionStateQueue:2068、dispatchActionState:2106、mountActionState:2380、updateActionState:2465
refReactFiberHooks.jsmountRef:2628、updateRef:2635
effectReactFiberHooks.jspushSimpleEffect/mountEffectImpl:2640、updateEffectImpl:2657、mountEffect:2697、updateEffect:2721、mountInsertionEffect:2782、mountLayoutEffect:2796
callback/memoReactFiberHooks.jsmountCallback:2923、updateCallback:2930、mountMemo:2944、updateMemo:2963
deferredValueReactFiberHooks.jsmountDeferredValue:2990、mountDeferredValueImpl:3028、updateDeferredValueImpl:3056
startTransitionReactFiberHooks.jsstartTransition:3117、mountTransition:3425、updateTransition:3443
派发ReactFiberHooks.jsdispatchSetStateInternal:3656、dispatchOptimisticSetState:3728、isRenderPhaseUpdate:3838、enqueueRenderPhaseUpdate:3846、entangleTransitionUpdate:3867
四个 dispatcherReactFiberHooks.jsContextOnlyDispatcher:3898、HooksDispatcherOnMount:3926、OnUpdate:3954、OnRerender:3982
useContext 底层ReactFiberNewContext.jsreadContext:553、readContextForConsumer:580、prepareToReadContext:540

21. 自测 FAQ(20 问)

Q1:useState 和 useReducer 到底什么关系? A:useState 是完全基于 useReducer 的语法糖,它的 reducer 是 basicStateReducer(ReactFiberHooks.js:1265)——传函数就执行、传值就用值。updateState 直接 return updateReducer(basicStateReducer, initialState)(:1962)。

Q2:为什么 setState 是稳定的、可以放进依赖数组? A:mountState 用 dispatchSetState.bind(null, fiber, queue) 在 mount 时创建一次(ReactFiberHooks.js:1953),之后更新渲染克隆 queue,queue.dispatch 引用不变,所以 setState 永远同一引用。

Q3:Hooks 的状态存在哪?为什么函数重新执行不丢? A:存在 workInProgress.memoizedState 上的单向链表(每个 hook 一个节点)。函数执行只是"读/写"这些节点,状态本身在 fiber 上,不随函数调用消亡。

Q4:为什么不能在 if/循环里写 hook? A:见第 7 章证据链。更新时 updateWorkInProgressHook(:1004)按"调用顺序"从 current 克隆节点;顺序变了会克隆到错误的旧状态,数量变了会在 :1048(more)或 :704(fewer)抛错。

Q5:渲染外调用 hook 会怎样? A:ReactSharedInternals.H 此时是 ContextOnlyDispatcher,除了 use/readContext 其余全部 throwInvalidHookError(ReactFiberHooks.js:3898/:445),报 "Invalid hook call"。

Q6:给 setState 传和当前一样的值,为什么不重渲染? A:触发 eager 优化(dispatchSetStateInternal,:3699)。当队列空且 is(eagerState, currentState) 为 true 时,调用 enqueueConcurrentHookUpdateAndEagerlyBailout 并 return false,不调度重渲染。

Q7:useEffect 的 deps 怎么比较? A:areHookInputsEqual(:456)用 Object.is(shared/objectIs,即 is)逐个比较。不传 deps(undefined)则 nextDeps = null,每次都执行。

Q8:useEffect 和 useLayoutEffect 执行时机有何不同? A:useEffect 打 PassiveEffect,commit 后由 scheduler 异步 flush(浏览器绘制后);useLayoutEffect 打 LayoutStaticEffect/UpdateEffect,在 commit 同步、绘制前执行,可同步读布局。

Q9:useRef 在 StrictMode 下为什么不"重置"? A:updateRef(:2635)直接返回 hook.memoizedState(mount 创建的对象),不重建。双调用之间该对象被复用,所以 ref.current 跨双调用保留(ref.current++ 会 +2)。

Q10:useMemo 能保证只算一次吗? A:不能,它只是优化提示,并发中断时可能丢弃重算。需"绝对只算一次"请用 useState 懒初始化或 useRef。

Q11:渲染阶段 setState 会怎样? A:走 enqueueRenderPhaseUpdate(:3846)入队并置 didScheduleRenderPhaseUpdate,renderWithHooks 检测到后 renderWithHooksAgain 再渲染一次。超过 25 次(RE_RENDER_LIMIT)抛错。

Q12:useContext 占 hook 链表吗? A:不占。它直接是 readContext(ReactFiberHooks.js:3931),把订阅挂到 fiber.dependencies.firstContext(ReactFiberNewContext.js:580),所以条件调用也不破 Rules of Hooks。

Q13:use 和 useContext 区别? A:use 还能读 thenable/promise 并 suspend(ReactFiberHooks.js:1159),是 React 19 统一入口;读 context 时 use(Ctx) 等价于 useContext(Ctx)。但 use(promise) 不能在 try/catch 中(靠抛异常工作)。

Q14:useOptimistic 的"自动回滚"怎么实现的? A:dispatchOptimisticSetState(:3728)给更新带 revertLane = requestTransitionLane(transition);updateReducerImpl(:1482)当 revertLane 对应 transition 已完成时 continue 跳过该更新,从而回滚到 passthrough 真实值。且 baseState 每帧重置为 passthrough(:2025)。

Q15:useActionState 返回什么,底层几个 hook? A:返回 [state, dispatch, isPending]。底层三个 hook:状态 hook(用 dispatchSetState)、pending hook(用 dispatchOptimisticSetState)、action 队列 hook(串行执行,:2380)。

Q16:并发更新里"低优先级更新不丢失"怎么保证? A:updateReducerImpl(:1316)遍历更新时,优先级不足(!isSubsetOfLanes(renderLanes, updateLane))的更新被 clone 进 newBaseQueue 并 markSkippedUpdateLanes,保留到 hook.baseQueue 供下次更高优先级渲染复用(详见第 9 章)。

Q17:为什么 useOptimistic 不走 eager 优化? A:mountOptimistic 故意把 lastRenderedReducer 设为 null(:1985),而 eager 优化要求 lastRenderedReducer !== null(:3684)。乐观更新不该被"提前 bailout"。

Q18:effect 在 fiber 上怎么存? A:挂在 fiber.updateQueue.lastEffect 的环形链表(pushEffectImpl,:2605)。每个 Effect 有 tag/create/deps/inst/next。

Q19:多个 useEffect 执行顺序是? A:按组件体内调用顺序,也就是 effect 环形链表从 lastEffect.next(首)到 lastEffect(尾)的顺序,先执行 destroy(上一次)再 create(本次)。

Q20:dispatcher 有哪几个,怎么切换? A:四个生产 dispatcher:ContextOnlyDispatcher(渲染外,抛错)、HooksDispatcherOnMount(首渲染)、HooksDispatcherOnUpdate(更新)、HooksDispatcherOnRerender(重渲染)。在 renderWithHooks(:563)按 current === null || current.memoizedState === null 切 mount/update,在 finishRenderingHooks(:660)切回 ContextOnlyDispatcher,重渲染时 renderWithHooksAgain(:791)切到 re-render。


21.5 Hook 链表的内存布局:三个指针如何协作

第 7 章前面讲过"hook 是挂在 fiber.memoizedState 上的单向链表"。这一节把链表的构建与遍历拆到源码级别,因为它是"为什么 hook 不能条件调用"的全部证据。

mount:尾插法建链

packages/react-reconciler/src/ReactFiberHooks.js:983

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

注意:第一个 hook 被同时赋给 fiber.memoizedState 与模块级变量 workInProgressHook;后续 hook 用 workInProgressHook = workInProgressHook.next = hook 尾插。因此链表顺序 = 调用顺序,没有任何名字、索引或键——这就是 hook 规则的全部依据。

update:双指针并行推进

:1004

function updateWorkInProgressHook(): Hook {
  // This function is used both for updates and for re-renders triggered by a
  // render phase update. It assumes there is either a current hook we can
  // clone, or a work-in-progress hook from a previous render pass that we can
  // use as a base.
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    // There's already a work-in-progress. Reuse it.
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
    // Clone from the current hook.
    if (nextCurrentHook === null) {
      const currentFiber = currentlyRenderingFiber.alternate;
      if (currentFiber === null) {
        throw new Error(
          'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
        );

这段揭示了两件事:

  1. 两个指针同步前进:currentHook 沿旧链表走,workInProgressHook 沿新链表走。每调用一次 hook,两者各前进一步。它们在位置上严格对应——不是靠名字,是靠次序。
  2. 重渲染(re-render)会复用已有的 WIP hook:if (nextWorkInProgressHook !== null) 分支说明渲染阶段更新触发的第二次渲染不会重新 clone,而是复用上一次 pass 已经建好的 hook。这让"渲染阶段 setState 后重跑渲染"变得便宜。

为什么条件调用必然出错

把上面的推进逻辑代入一个条件分支:

if (cond) {
  useState();   // 第 1 次调用
}
useEffect();    // cond 为 false 时,它变成了第 1 次调用
  • 首次渲染 cond === true:链表是 [useState, useEffect]。
  • 二次渲染 cond === false:useEffect 成为第 1 次调用 → updateWorkInProgressHook 试图 clone 第 1 个 hook(那是 useState 的 hook)→ 类型不匹配,后续 updateEffectImpl 读到的是 state 的 queue,直接崩溃或行为错乱。

如果反过来(这次调用得更多),会走到 nextCurrentHook === null 且 currentFiber !== null 的分支,抛出 "Rendered more hooks than during the previous render"。这就是为什么 React 的报错能精确到"多了"还是"少了"——因为链表推进是严格位置对应的。


21.6 渲染阶段更新:为什么允许,25 次上限从哪来

在渲染函数里调用 setState 是被允许的(不像一些框架直接禁止),React 会立即重跑这个组件的渲染。它是"派生状态的兜底写法"(例如"props 变化时重置 state"),官方推荐的替代方案是渲染期直接比较并赋值。

上限常量

:295

const RE_RENDER_LIMIT = 25;

:810 起的循环:

  let numberOfReRenders: number = 0;
  ...
    if (numberOfReRenders >= RE_RENDER_LIMIT) {
      ...
    }
  ...
    numberOfReRenders += 1;

超过 25 次会抛 "Too many re-renders. React limits the number of renders to prevent an infinite loop."。这个数字是经验阈值:正常的"渲染期调整状态"一次就收敛,25 次足够覆盖合法场景,又能快速终止死循环。

入队路径

enqueueRenderPhaseUpdate(:3846)把更新放进 queue.pending,并置 didScheduleRenderPhaseUpdate = true。渲染结束后 renderWithHooks 检测到这个标志,会重新调用组件函数(而不是走完整的调度流程——因为还在同一个渲染任务里)。

三条实践结论

  1. 渲染期 setState 不会触发新的调度:它在当前渲染内同步重跑,所以不会让 isPending 变化,也不会被时间切片。
  2. 上限 25 是"整个渲染过程"的累计,不是每次 hook 的。多个 hook 各触发一次也会累计到同一个计数器。
  3. 能不用就不用:绝大多数场景应该改成"渲染期比较并直接赋值"(官方推荐的 "adjusting state when props change" 模式),或者在 effect 里处理。渲染期 setState 会让这次渲染至少跑两遍。

21.7 三种 effect 的执行时机与标志对照

React 19 有三种 effect,它们共用同一套存储结构(fiber.updateQueue.lastEffect 环形链表),靠 tag 位区分:

标志(ReactHookEffectTags.js)Hook执行时机典型用途
HookInsertionuseInsertionEffectcommit 的 mutation 阶段之前,同步CSS-in-JS 库在读取布局前注入样式
HookLayoutuseLayoutEffectcommit 的 layout 阶段,同步,DOM 已更新测量 DOM、同步改样式避免闪烁
HookPassiveuseEffectcommit 之后,由 Scheduler 异步 flush数据请求、订阅、日志

对应的 mount 实现:

2640  function mountEffectImpl(...)          // Passive
2782  function mountInsertionEffect(...)     // Insertion
2796  function mountLayoutEffect(...)        // Layout
2657  function updateEffectImpl(...)         // 三种共用同一套 deps 比较逻辑

关键差异只有两点:tag 位决定它在 commit 的哪一段被执行;时机决定它是否阻塞绘制。

  • useInsertionEffect 与 useLayoutEffect 都是同步的,会阻塞绘制。区别是前者更早(在 DOM 变更之前),因此拿不到最新 DOM——它的设计目标就是"在读取布局前插入 <style>",普通业务代码不该用。
  • useEffect 是异步的(第 6 章讲的 flushPassiveEffects),不阻塞绘制。但它仍在主线程上跑,重计算放进去照样造成长任务。

updateEffectImpl 的 deps 比较(areHookInputsEqual)对三种 effect 完全一致:deps 未变就不执行,也不重新登记。所以"三种 effect 的 deps 语义相同",差别只在执行时机。


21.8 useSyncExternalStore:并发渲染下 tearing 的唯一正解

tearing 是什么

tearing(撕裂)指同一次渲染中,不同组件读到了外部数据源的不同版本。在同步渲染下不会发生:渲染是一个不可中断的整体。但在并发渲染下,渲染可能被让出——如果外部 store 在让出期间发生变化,先渲染的组件读到旧值、后渲染的读到新值,UI 就"撕裂"了。

Hook 的解法

mountSyncExternalStore(:1648)与 updateSyncExternalStore(:1739)的实现要点:

  1. getSnapshot 在渲染期被调用,返回值参与这次渲染。
  2. 订阅在 commit 阶段建立(useEffect 语义),保证渲染期间不会被通知。
  3. 每次渲染都会重新读 getSnapshot 并与上次比较,如果检测到"渲染期间发生了变化",React 会强制同步重渲染(走 sync lane),从而让整棵树读到同一个版本。

这就是为什么这个 hook 能防 tearing:它用"检测 + 强制同步重试"把并发的可中断性对外部数据源屏蔽掉。

三个必须遵守的约束

  1. getSnapshot 必须返回缓存后的值。如果它每次返回新对象({a, b}),React 会认为 store 一直在变 → 无限重渲染。源码里这个比较用的是 is()。
  2. 必须提供 getServerSnapshot(SSR 场景),否则服务端渲染会抛 "Missing getServerSnapshot"。因为服务端没有订阅机制,只能取一次快照。
  3. 订阅的回调必须是稳定的,否则每次渲染都会重新订阅/退订。

为什么不用 useState + useEffect 手写

手写版本无法处理"渲染期间 store 变化"这一情况——你的 effect 在 commit 之后才订阅,中间的变化会丢失,或者读到不一致的值。useSyncExternalStore 是 React 专门为这个并发语义提供的原语,也是所有状态管理库(Redux、Zustand 等)在 React 18+ 必须接入的原因。


22. 本章要点小结

  1. 存储模型:组件所有 hook 以单向链表挂在 fiber.memoizedState;Hook 含 memoizedState/baseState/baseQueue/queue/next;副作用型 hook 的 Effect 以环形链表挂在 fiber.updateQueue.lastEffect;useContext 不占链表,挂 fiber.dependencies.firstContext。
  2. Dispatcher 机制:ReactSharedInternals.H 全局指针在 renderWithHooks 按 mount/update/rerender 切换,渲染外是 ContextOnlyDispatcher(全抛错)。这是"hook 不能在渲染外/条件里调用"的强制保障。
  3. 链表必然性:函数组件每次执行都重新跑,状态必须存活在 fiber 上;链表按调用顺序组织,与 Rules of Hooks 严丝合缝。
  4. 状态更新:useState 是 useReducer 特例(basicStateReducer);更新存于 queue.pending 环形链表,dispatch 稳定引用;updateReducerImpl 按 lane 过滤,低优先级更新被 clone 进 baseQueue 保留(复用与丢弃);eager 优化在"队列空且算出 state 未变"时短路 bailout。
  5. 渲染阶段更新:走 enqueueRenderPhaseUpdate,触发最多 25 次重渲染(RE_RENDER_LIMIT),靠 renderWithHooksAgain 就地消费。
  6. effect:mountEffectImpl 打 PassiveEffect 标记 + 挂 updateQueue;updateEffectImpl 用 areHookInputsEqual(Object.is)比较 deps,未变则跳过;Passive effect 在 commit 后由 scheduler 异步 flush。Passive/Layout/Insertion 三种标记的 fiber 标记与执行时机不同。
  7. ref 稳定性:updateRef 直接复用 memoizedState,永不重建;StrictMode 下双调用仍共享同一对象。
  8. useMemo/useCallback:靠 areHookInputsEqual 短路;useCallback 是 useMemo 的语法糖。
  9. React 19 新 hook:use 统一读 Context/Promise(可 suspend);useOptimistic 每帧重置 baseState 为 passthrough + revertLane 实现乐观叠加与自动回滚;useActionState 是 useState + 乐观 pending + 串行 action 队列;useDeferredValue/useTransition 把非紧急更新放到低优先级车道。

理解本章后,你对 React 函数组件"为什么能这样工作、为什么不能那样写"会有从根上的把握。下一章(如有)可深入并发渲染中 hook 与车道系统的交互细节。


本章所有行号基于 React 19.3.1 源码(react-19.3.1),引用前已在对应文件中核对。如运行时版本不同,请以实际源码为准。