Skip to content

第 3.3 节:调度任务:scheduleUpdateOnFiber 的职责

在上一节中,我们探讨了 dispatchSetState 如何创建一个 Update 对象并将其放入 updateQueue。然而,这仅仅是更新流程的第一步。创建 Update 对象后,React 需要一个机制来“通知”调度器有新的工作需要处理。这个关键的衔接步骤正是由 scheduleUpdateOnFiber 函数完成的。

scheduleUpdateOnFiber 是 React 调度系统的“入口点”。每当发生状态变更、forceUpdate 或其他需要重新渲染的事件时,它都会被调用,以确保更新任务被正确地注册和调度。

1. scheduleUpdateOnFiber 的核心源码

scheduleUpdateOnFiber 函数位于 packages/react-reconciler/src/ReactFiberWorkLoop.js 中。虽然它代码不长,但其职责至关重要。

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

export function scheduleUpdateOnFiber(
  root: FiberRoot,
  fiber: Fiber,
  lane: Lane,
) {
  // ... (DEV-only checks)

  // 1. 标记 Root 有待处理的更新
  markRootUpdated(root, lane);

  // 2. 确保 Root 已经被调度
  ensureRootIsScheduled(root);

  // ... (处理遗留模式下的同步更新)
}

从源码中可以看出,scheduleUpdateOnFiber 的核心逻辑主要分为两步:

  1. 标记更新(markRootUpdated:将新的更新所属的 lane 添加到 FiberRootpendingLanes 集合中。这就像在一个任务板上标记“有新任务待处理”。
  2. 确保调度(ensureRootIsScheduled:检查 FiberRoot 是否已经有一个调度任务在进行中。如果没有,它会创建一个新的调度任务,并将其交给底层的调度器(Scheduler)。

接下来,我们将深入分析这两个步骤。

2. 标记更新:markRootUpdated

markRootUpdated 的作用是更新 FiberRoot 上的 pendingLanespendingLanes 是一个位掩码(Bitmask),用于跟踪所有待处理的更新的优先级。

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

export function markRootUpdated(root: FiberRoot, updatedLanes: Lanes) {
  root.pendingLanes |= updatedLanes;

  // ...
}

这里的 |= 操作(按位或赋值)非常关键。它将新的 updatedLanes 合并到 root.pendingLanes 中。例如,如果 pendingLanes0b0010,新的 updatedLanes0b0100,那么合并后的结果就是 0b0110。这意味着现在有两个不同优先级的任务在等待处理。

设计思考:为什么不直接替换而是使用位掩码合并?

  • 批量处理:React 的一个核心优化是批量处理更新。在一个事件循环中可能触发多次 setState。通过使用位掩码,React 可以将这些更新合并到一次渲染中,从而避免不必要的重复工作。
  • 优先级管理:不同的更新有不同的优先级(例如,用户输入的优先级高于数据请求)。位掩码使得 React 可以同时跟踪多个优先级的任务,并在调度时选择最重要的任务来执行。

3. 确保调度:ensureRootIsScheduled

标记完更新后,ensureRootIsScheduled 函数确保这个 FiberRoot 上的工作已经被安排。

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

export function ensureRootIsScheduled(root: FiberRoot) {
  const existingCallbackNode = root.callbackNode;
  
  // ...

  // 如果已经有调度任务,并且优先级足够,则无需重复调度
  if (existingCallbackNode !== null) {
    const existingCallbackPriority = root.callbackPriority;
    if (existingCallbackPriority >= newCallbackPriority) {
      // The existing callback is sufficient.
      return;
    }
    // 取消旧的、优先级较低的任务
    cancelCallback(existingCallbackNode);
  }

  // ...

  // 创建一个新的调度任务
  let newCallbackNode;
  if (newCallbackPriority === SyncLanePriority) {
    // 同步任务
    newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
  } else {
    // 其他优先级的任务
    const schedulerPriorityLevel = lanePriorityToSchedulerPriority(newCallbackPriority);
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }

  root.callbackNode = newCallbackNode;
  root.callbackPriority = newCallbackPriority;
}

ensureRootIsScheduled 的逻辑可以概括为:

  1. 检查现有任务:查看 root.callbackNode 是否存在。callbackNode 是底层调度器返回的任务句柄。
  2. 优先级比较:如果存在任务,比较现有任务的优先级和当前新任务的优先级。如果现有任务的优先级已经足够高(即能够处理当前这个新任务),则无需做任何事。
  3. 取消低优任务:如果新任务的优先级更高,那么会取消掉旧的、优先级较低的任务,以便让高优任务“插队”。
  4. 调度新任务:如果没有现有任务,或者新任务优先级更高,它会调用 scheduleCallback(或 scheduleSyncCallback)来向调度器注册一个新任务。这个任务的执行函数是 performConcurrentWorkOnRootperformSyncWorkOnRoot,它们是下一阶段——Render 阶段的入口。

设计思考:为什么需要“确保”而不是“总是”调度?

  • 性能优化:频繁地创建和取消调度任务是有成本的。如果 React 已经在计划执行一次高优先级的渲染,那么后续到达的低优先级更新可以“搭便车”,在同一次渲染中被处理,而无需创建一个新的调度任务。
  • 保证一致性:通过这种机制,React 确保在任何时间点,一个 FiberRoot 最多只有一个活跃的调度任务。这简化了状态管理,并避免了多个渲染任务相互冲突。

4. 案例:一次点击事件中的多次 setState

假设在一个点击事件处理器中,我们连续调用了两次 setState

jsx
function MyComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  function handleClick() {
    // 第一次 setState
    setCount(c => c + 1); 
    // 第二次 setState
    setText('hello');
  }

  return <div onClick={handleClick}>{count} - {text}</div>;
}
  1. 第一次 setCount

    • dispatchSetState 创建一个 Update 对象。
    • scheduleUpdateOnFiber 被调用。假设这是一个用户交互,laneSyncLane
    • markRootUpdatedroot.pendingLanes 标记为 SyncLane
    • ensureRootIsScheduled 发现没有现存任务,于是通过 scheduleSyncCallback 调度了一个同步任务,并将 callbackNode 存到 root 上。
  2. 第二次 setText

    • dispatchSetState 再次创建一个 Update 对象。
    • scheduleUpdateOnFiber 再次被调用,lane 同样是 SyncLane
    • markRootUpdated 再次标记 root.pendingLanes(由于已经是 SyncLane,所以不变)。
    • ensureRootIsScheduled 发现 root.callbackNode 已经存在,并且优先级相同。因此,它什么也不做,直接返回。

最终,虽然有两次 setState 调用,但只调度了一次渲染任务。在这次渲染中,React 会处理 updateQueue 中的所有 Update 对象,从而将 counttext 的变更一次性应用。这就是 React 更新批处理(Batching)的核心实现。

通过 scheduleUpdateOnFiber,React 巧妙地将组件状态的变更意图,转化为了一个可被调度器管理的、带有优先级的渲染任务,为后续的 Render 和 Commit 阶段铺平了道路。

Last updated: