Skip to content

第 3.2 节:事件触发与创建 Update

一次 React 更新的旅程,始于一个状态变更函数的调用,通常是 useState 返回的 dispatch 函数(如 setCount)。这个看似简单的函数调用,是启动整个 React 渲染管线的“第一张多米诺骨牌”。本节将深入源码,探究这张牌倒下时,React 内部究竟做了什么。

dispatch 函数的真面目

我们在调用 useState 时,获取到的 dispatch 函数并不是一个独立的、全局的函数。在组件首次渲染(mount)时,React 通过 .bind 方法,将当前 Fiber 节点和该 state 对应的 updateQueue 与一个名为 dispatchSetState 的内部函数进行了绑定。

文件定位packages/react-reconciler/src/ReactFiberHooks.js

javascript
// mountState 函数内部
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountStateImpl(initialState);
  const queue = hook.queue;
  // 关键在这里!
  const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber, // 绑定当前 Fiber
    queue,                   // 绑定 Hook 的 updateQueue
  ): any);
  queue.dispatch = dispatch;
  return [hook.memoizedState, dispatch];
}

这种设计的精妙之处在于:

  • 上下文感知dispatch 函数“天生”就知道它应该更新哪个组件(Fiber)的哪个状态(queue)。
  • 闭包:每个 useState 调用都创建了一个独立的闭包,确保了状态更新的隔离性。

dispatchSetState:更新的起点

当我们调用 setCount(1) 时,实际上是在执行 dispatchSetState(fiber, queue, 1)。这个函数是整个更新流程的入口,它的核心职责有三件:

  1. 确定更新的优先级(Lane)
  2. 创建一个 Update 对象
  3. Update 对象入队(enqueue),并调度一次新的渲染

让我们看看 dispatchSetState 的简化版实现:

javascript
// packages/react-reconciler/src/ReactFiberHooks.js

function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
): void {
  // 1. 获取当前更新的优先级 Lane
  const lane = requestUpdateLane(fiber);

  // 2. 创建一个 Update 对象
  const update: Update<S, A> = {
    lane: lane,
    action: action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
    // ...其他字段
  };

  // 3. 将 Update 对象入队
  const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
  if (root !== null) {
    // 4. 调度渲染
    scheduleUpdateOnFiber(root, fiber, lane);
  }
}

1. Update 对象:更新的原子单元

React 并不直接处理 action(例如我们传入的新 state 1),而是将其封装在一个 Update 对象中。这个对象是 React 更新机制的原子单元。

javascript
// packages/react-reconciler/src/ReactFiberHooks.js

export type Update<S, A> = {
  lane: Lane,         // 此更新的优先级
  revertLane: Lane,   // 用于恢复的 Lane
  action: A,          // 更新的载荷,可以是新 state 或一个函数 (s => s + 1)
  hasEagerState: boolean, // 是否可以提前计算 state
  eagerState: S | null,   // 提前计算出的 state
  next: Update<S, A>, // 指向下一个 Update 对象,构成环形链表
};

2. updateQueue:待办事项列表

每个 Hook 都有一个 updateQueue,它像一个待办事项列表,存储着所有等待处理的 Update 对象。

javascript
export type UpdateQueue<S, A> = {
  pending: Update<S, A> | null, // 一个指向最后一个 Update 的指针,形成环形链表
  lanes: Lanes,                 // 队列中所有 Update 的优先级集合
  dispatch: (A => mixed) | null,
  lastRenderedReducer: ((S, A) => S) | null,
  lastRenderedState: S | null,
};

enqueueConcurrentHookUpdate 函数会将新的 Update 对象添加到 queue.pending 构成的环形链表中。使用环形链表(只存一个 last 指针)是一种空间优化,通过 last.next 即可轻松找到 first,同时添加新 Update 也非常高效。

3. scheduleUpdateOnFiber:请求渲染

Update 入队后,dispatchSetState 会调用 scheduleUpdateOnFiber。这个函数是连接“状态更新”和“启动渲染”两个世界的桥梁。它不会立即开始渲染,而是通知 React 的调度中心:“嘿,有一个新的更新来了,它的优先级是 lane,请安排一下工作!”

scheduleUpdateOnFiber 的具体职责我们将在下一节详细探讨。

设计思想解读

为什么不直接更新 state,而是要设计这样一套复杂的 UpdateQueue 机制?

  • 解耦与异步:将“更新意图”(调用 setState)和“更新执行”(Render 阶段)解耦。这使得 React 可以在两者之间进行优先级判断、任务调度、批量处理(batching)等异步操作,是实现并发渲染的基础。
  • 状态来源的确定性:所有状态的变更都来自于一个可追溯的 Update 对象队列。这使得状态的计算过程变得确定和可预测,也为时间旅行(Time-traveling)等调试工具提供了可能。
  • 批量更新(Batching):在一个事件循环中多次调用 setState,React 会将它们创建的多个 Update 对象放入同一个队列中,然后在一次渲染中合并处理。这是 React 性能优化的一个重要手段。

通过这套机制,React 将一个简单的 setState 调用,成功转化为一个结构化、可调度、可追踪的内部任务,为后续的黄金路径铺平了道路。

Last updated: