Skip to content

react-dom、scheduler 与更新调度源码主线

前面看完 Element、Fiber 和 Hooks 之后,React 源码还剩两条很关键的线:

  1. react-dom 怎么把 React 树接到浏览器或服务端
  2. scheduler 怎么安排任务优先级和执行时机

这两条线如果不接上,源码主线就会断在“React 算完了,但到底怎么落地、怎么安排执行”这里。

这篇文章重点讲:

  1. createRoothydrateRoot 在源码主线上接在哪里
  2. scheduleUpdateOnFiber 为什么是重要入口
  3. lanes 和优先级该怎么理解
  4. scheduler 在 React 里到底扮演什么角色

1. react-dom 到底在源码层负责什么

react-dom 更偏宿主接入层。

它主要解决的是:

  1. 浏览器里的根节点怎么创建
  2. React 树怎么挂到 DOM 容器上
  3. 服务端 HTML 怎么水合
  4. commit 阶段的宿主操作怎么真正落到 DOM

把这层分工拉开:

  1. react 定义模型
  2. react-reconciler 负责算下一棵树
  3. react-dom 负责接浏览器和服务端环境

1.1 react-dom 的完整释义和能力边界

如果只说 react-dom 是“React 操作 DOM 的包”,这个定义还是偏窄。

更完整的理解应该是:

react-dom 是 React 面向浏览器 DOM 和服务端 HTML 输出时的宿主接入层。它负责创建 root、把 commit 阶段的宿主变更真正落到 DOM、处理水合,以及提供服务端输出接口。`

它主要负责:

  1. 浏览器 root 创建与挂载
  2. 水合接管
  3. commit 阶段的宿主节点创建、更新、删除
  4. 服务端输出入口,例如字符串或流式 HTML

它不主要负责:

  1. 不定义 React Element 模型
  2. 不独立完成 Fiber 调和
  3. 不单独决定所有更新优先级

所以 react-dom 的边界很明确:它是宿主接入和落地层,不是完整内核本身。


2. createRoothydrateRoot 接在哪条线

在客户端入口里,你最常见到:

js
import { createRoot, hydrateRoot } from "react-dom/client";

2.1 createRoot

它更适合纯客户端挂载。

主线可以看成:

  1. 创建一个 root 容器对象
  2. 初始化根 Fiber
  3. 调用 root.render(...)
  4. 进入更新调度流程

2.2 hydrateRoot

它更适合服务端 HTML 已经先到页面上的场景。

主线是:

  1. 找到已有 DOM
  2. 创建 root
  3. 以“水合模式”进入后续更新和事件接管流程

也就是说,这两个 API 的差别不只是名字,而是当前页面到底有没有现成的服务端 HTML 可接管。


3. 一条从 setState 到 DOM 更新的源码线

如果把一次状态更新压成一条更完整的源码主线,通常可以这样看:

mermaid
flowchart TD
    A[dispatchSetState] --> B[scheduleUpdateOnFiber]
    B --> C[标记 lane / 优先级]
    C --> D[进入 render work loop]
    D --> E[beginWork / completeWork]
    E --> F[commitRoot]
    F --> G[react-dom 执行 DOM 变更]

这条线里最值得盯住的入口,就是 scheduleUpdateOnFiber

因为它意味着:

  1. 当前 Fiber 有新更新了
  2. 这次更新属于什么优先级
  3. 接下来是否要尽快安排 render

4. scheduleUpdateOnFiber 为什么重要

React 不是哪儿改了状态就直接哪儿改 DOM。

它会把“这个 Fiber 上有更新”这件事统一交给调度入口处理。

可以把它理解成:

  1. 给当前更新打标签
  2. 把更新一路标记到根节点
  3. 决定这次任务接下来怎么被安排执行

精简后的思路示意:

js
function scheduleUpdateOnFiber(root, fiber, lane) {
  markRootUpdated(root, lane);
  ensureRootIsScheduled(root);
}

这里最关键的不是函数名,而是这两个动作:

  1. markRootUpdated
  2. ensureRootIsScheduled

也就是说,更新不是局部偷偷发生,而是会统一回到根节点调度。


5. lane 到底怎么理解

现代 React 源码里,一个很关键的词就是 lane

可以把它理解成 一组表示更新优先级和调度类别的位标记。

你不用一开始就钻所有 lane 常量,但一定要先知道:

  1. React 不再只靠一个简单“同步 / 异步”二分法
  2. 不同更新可以有不同优先级
  3. React 会根据 lane 决定谁先算、谁可以稍后处理

例如概念上常见会区分:

  1. 用户输入这类更紧急更新
  2. 过渡性更新
  3. 空闲时再做的更新

所以 lane 更像“更新车道”,而不是普通数字序号。


5.1 lane 的能力边界

lane 很容易被误解成“新的优先级常量名”。

lane 真正负责的是:

  1. 给更新打类别和优先级标记
  2. 让 root 能知道当前有哪些更新待处理
  3. 让 React 在多类更新之间做选择

它不负责的是:

  1. 不自己执行调和
  2. 不自己做 DOM commit
  3. 不等于调度器本身

所以 lane 更像“更新标签体系”,而 scheduler 更像“根据这些标签安排执行的系统”。


6. scheduler 在 React 里扮演什么角色

很多人会把 scheduler 和 Fiber 混成一件事。

把这两层职责分开看:

能力更偏解决什么
Fiber更新工作如何表示和执行
scheduler更新任务何时执行、谁优先执行

也就是说,Fiber 更偏“工作内容”,scheduler 更偏“工作安排”。

它的价值主要在这里:

  1. 避免所有更新都抢在同一时刻同步做完
  2. 让更重要的交互优先得到响应
  3. 给并发能力提供基础

6.1 scheduler 的完整释义和能力边界

scheduler 很容易被说成“React 的调度器”。

这个说法方向没错,但还需要补完整:

scheduler 更像 React 所依赖的一层通用任务调度能力。它负责根据优先级安排回调何时执行,让 React 不必把所有更新都一次性同步做完。`

它主要负责:

  1. 按优先级安排回调
  2. 让任务可分批、可打断、可延后
  3. 给并发相关能力提供执行基础

它不主要负责:

  1. 不理解 Fiber 节点内部结构
  2. 不自己生成 React 子树
  3. 不直接执行 DOM 操作

所以把这几层关系放回原位看:

  1. lane 告诉 React“这类更新是什么”
  2. scheduler 负责“这类更新什么时候跑”
  3. reconciler 负责“跑的时候怎么算”

7. 一份调度思路示意

下面这段代码仍然是保留主线后的阅读辅助版本:

js
function ensureRootIsScheduled(root) {
  const nextLanes = getNextLanes(root);
  const priority = lanesToSchedulerPriority(nextLanes);

  scheduleCallback(priority, () => {
    performConcurrentWorkOnRoot(root);
  });
}

这段示意最值得看的 3 件事是:

  1. 先从 root 里取“下一批最该处理的 lanes”
  2. 再把 React 自己的 lane 转成调度器优先级
  3. 最后交给 scheduleCallback(...) 安排执行

所以调度并不是“某个 Hook 自己偷偷跑起来”,而是统一从 root 入口安排。


8. react-dom 在 commit 阶段做什么

到了 commit 阶段,React 已经知道哪些节点该插入、更新、删除。

这时 react-dom 真正重要的事情是:

  1. 创建 DOM 实例
  2. 更新属性
  3. 插入和删除 DOM 节点
  4. 处理文本节点
  5. 绑定或解绑事件

也就是说,react-reconciler 更偏“决定做什么”,react-dom 更偏“在浏览器里真正执行这些宿主操作”。


8.1 react-reconcilerreact-dom 在 commit 上的边界

这两个包在 commit 这里最容易串线。

可以直接这样区分:

react-reconciler

更偏:

  1. 知道哪些 Fiber 有哪些 flags
  2. 知道当前该插入、更新还是删除
  3. 组织 commit 阶段的执行流程

react-dom

更偏:

  1. 真正创建 DOM 实例
  2. 真正更新属性和文本
  3. 真正把节点插入或移除

也就是说:

  1. react-reconciler 更像“决策和流程组织层”
  2. react-dom 更像“浏览器宿主执行层”

如果不把这层边界讲清楚,就很容易误以为 commit 全都在 react-dom 或全都在 react-reconciler


9. 客户端和服务端两条线怎么分

源码里其实会越来越明显地区分这两条线。

9.1 客户端

更常见的关注点是:

  1. createRoot
  2. hydrateRoot
  3. commit 阶段的 DOM 操作

9.2 服务端

更常见的关注点是:

  1. renderToString
  2. 流式渲染
  3. 输出 HTML,而不是直接操作浏览器 DOM

也就是说,服务端这条线不是“换个地方跑 React DOM”,而是宿主目标本身就变了。


10. 常见相关源码文件

如果你继续顺着仓库看,通常会碰到这些位置:

文件主要职责
packages/react-dom/src/client/ReactDOMRoot.jscreateRoot / hydrateRoot 等客户端入口
packages/react-dom/src/client 下相关文件DOM 根节点、容器初始化
packages/scheduler/src/forks/Scheduler.js调度器主线
packages/react-reconciler/src/ReactFiberWorkLoop.jsrender 与调度主循环连接点

建议先重点看:

  1. root 是怎么创建的
  2. 更新如何从 Fiber 回到 root 调度
  3. lane 怎样被挑选和调度
  4. commit 后半段怎么落到宿主环境

11. 这一层最常见的误区

11.1 以为 react-dom 负责全部 React 内核

不是。

它更偏宿主接入。

11.2 以为 scheduler 就是 Fiber

不是。

Fiber 更偏数据结构和执行模型,scheduler 更偏任务安排。

11.3 以为所有更新优先级都一样

现代 React 明显不是这样。

lane 和 scheduler 正是在解决这件事。


12. 一句话总结

react-domscheduler 这条源码主线,本质上是在做 把更新从 Fiber 节点统一提升到 root 调度,按 lane 和优先级安排执行,再由 react-dom 把最终 commit 结果真正落到浏览器 DOM 或服务端输出。

基于 VitePress 构建的个人技术笔记。