内部实现:React

发布: (2025年12月12日 GMT+8 21:44)
6 min read
原文: Dev.to

Source: Dev.to

介绍

自从我开始使用 React 起,我就想弄清楚它的内部工作原理。这不是对源码的细粒度审查,而是对 React 内部一些关键包和集成的概览。进行这项研究帮助我更好地理解 React 并更有信心地调试问题。希望你也能获得更好的视角。

层次结构

我发现,React 本身作为一个独立的包并不是很有用。它充当 API 层,并大量委托给 react-reconciler 包。这也是它常常与渲染器(如 react-dom)一起使用的原因,后者与 react-reconciler 捆绑在一起。

因此,要了解 React,就必须了解 react-reconciler 以及它的关键依赖——scheduler 包。我们先从 scheduler 开始。

scheduler

scheduler 包通过将工作拆分成小块并管理执行优先级,使 React 的并发特性得以实现。

“对于同步渲染,一旦更新开始渲染,直到用户在屏幕上看到结果之前,任何事情都无法中断它。”
— React v18.0 | 2022 年 3 月 29 日,React 团队

在并发 React 出现之前,更新是同步的,会阻塞主线程直至完成,这导致 UI 卡顿和动画不流畅。

scheduler 通过实现 协作调度 来解决这个问题:任务自愿将控制权让回给宿主,以便主线程可以用于其他工作。任务运行一个短时间间隔(≈ 5 ms),检查是否应该让出,如果截止时间已过则让出,然后等待下一次执行机会。

协作调度由 消息循环 管理,而实际的工作则由 工作循环 处理。

任务、两个循环和两个队列

unstable_scheduleCallback 函数(以下简称 scheduleCallback)是调度并发(非同步)工作的主要方式。

function unstable_scheduleCallback(
  priorityLevel: PriorityLevel,
  callback: Callback,
  options?: { delay: number },
): Task {
  // implementation...
}

Task 对象

export opaque type Task = {
  id: number,
  callback: Callback | null,
  priorityLevel: PriorityLevel,
  startTime: number,
  expirationTime: number,
  sortIndex: number,
  isQueued?: boolean,
};

优先级

export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;

export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;

scheduler 使用这些级别来管理执行优先级。

队列

scheduler 维护两个优先级队列(使用最小堆实现):

队列用途
Task queue存放已准备好执行的任务。
Timer queue存放尚未满足条件的任务(它们的 startTime 在未来)。

创建任务时,会为其分配一个 startTime(任务何时可执行)和一个 expirationTime(任务必须在何时之前完成)。

  • 如果 startTime > currentTime,任务进入 Timer queue
  • 否则,直接进入 Task queue
计算 startTime
// 立即任务
startTime = currentTime; // 现在可用

// 延迟任务
startTime = currentTime + delay; // 将来可用
计算 expirationTime
expirationTime = startTime + timeout;

timeout 取决于 PriorityLevel,从 ImmediatePriority-1 msLowPriority 大约 10 000 ms 不等。

sortIndex
  • Timer queue 中,sortIndex 等于 startTime
  • Task queue 中,sortIndex 等于 expirationTime

优先级示例

优先级层级常见触发场景
ImmediatePriority(同步)在现代 React 中很少使用
UserBlockingPriorityonScrollonDragonMouseMove
NormalPrioritystartTransition()useDeferredValue(大多数调度工作默认)
IdlePriority离屏/隐藏内容的渲染

同步工作会绕过调度器,使用 queueMicrotask API 加入队列。

工作循环

function workLoop(initialTime: number) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  while (currentTask !== null) {
    if (!enableAlwaysYieldScheduler) {
      if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
        // 任务尚未超时且已到达截止时间。
        break;
      }
    }
    // ... execute callback ...
    currentTask = peek(taskQueue);
  }
}

工作循环从 Task queue 中取出任务并执行其回调,直到必须让给宿主为止。它还会通过 advanceTimers 将 Timer queue 中的任务提升到 Task queue。如果任务运行时间超过了分配的时间片,其 callback 会被替换为 continuation callback,以便调度器稍后继续执行。被取消的任务在出队时会被丢弃。

消息循环

当工作循环让出时,React 需要一种机制来重新启动它。这由 消息循环 负责。React 更倾向使用非标准的 setImmediate API;如果不可用,则回退到 MessageChannelsetTimeout(用于非浏览器环境)。

const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
const schedulePerformWorkUntilDeadline = () => {
  port.postMessage(null);
};

performWorkUntilDeadline 启动工作循环。当工作循环决定让出时,它会调用 schedulePerformWorkUntilDeadline(),该函数发送一条消息,在下一个 tick 触发 performWorkUntilDeadline。使用 MessageChannel 可以避免 setTimeout 强加的 4 ms 最小延迟,从而让 React 更精确地调度工作。

Back to Blog

相关文章

阅读更多 »

React 使用计算器

今天我完成了一个使用 React 的练习项目——计算器。这个 React Calculator 应用程序执行基本的算术运算。它支持按钮…

Reatom:随你成长的状态管理

碎片化问题 现代前端开发有一个常见的模式: - 从简单的 useState hook 开始 - 需要共享状态?添加 Context - Context re‑...