Appearance
react-dom、scheduler 与更新调度源码主线
前面看完 Element、Fiber 和 Hooks 之后,React 源码还剩两条很关键的线:
react-dom怎么把 React 树接到浏览器或服务端scheduler怎么安排任务优先级和执行时机
这两条线如果不接上,源码主线就会断在“React 算完了,但到底怎么落地、怎么安排执行”这里。
这篇文章重点讲:
createRoot、hydrateRoot在源码主线上接在哪里scheduleUpdateOnFiber为什么是重要入口- lanes 和优先级该怎么理解
scheduler在 React 里到底扮演什么角色
1. react-dom 到底在源码层负责什么
react-dom 更偏宿主接入层。
它主要解决的是:
- 浏览器里的根节点怎么创建
- React 树怎么挂到 DOM 容器上
- 服务端 HTML 怎么水合
- commit 阶段的宿主操作怎么真正落到 DOM
把这层分工拉开:
react定义模型react-reconciler负责算下一棵树react-dom负责接浏览器和服务端环境
1.1 react-dom 的完整释义和能力边界
如果只说 react-dom 是“React 操作 DOM 的包”,这个定义还是偏窄。
更完整的理解应该是:
react-dom 是 React 面向浏览器 DOM 和服务端 HTML 输出时的宿主接入层。它负责创建 root、把 commit 阶段的宿主变更真正落到 DOM、处理水合,以及提供服务端输出接口。`
它主要负责:
- 浏览器 root 创建与挂载
- 水合接管
- commit 阶段的宿主节点创建、更新、删除
- 服务端输出入口,例如字符串或流式 HTML
它不主要负责:
- 不定义 React Element 模型
- 不独立完成 Fiber 调和
- 不单独决定所有更新优先级
所以 react-dom 的边界很明确:它是宿主接入和落地层,不是完整内核本身。
2. createRoot 和 hydrateRoot 接在哪条线
在客户端入口里,你最常见到:
js
import { createRoot, hydrateRoot } from "react-dom/client";2.1 createRoot
它更适合纯客户端挂载。
主线可以看成:
- 创建一个 root 容器对象
- 初始化根 Fiber
- 调用
root.render(...) - 进入更新调度流程
2.2 hydrateRoot
它更适合服务端 HTML 已经先到页面上的场景。
主线是:
- 找到已有 DOM
- 创建 root
- 以“水合模式”进入后续更新和事件接管流程
也就是说,这两个 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。
因为它意味着:
- 当前 Fiber 有新更新了
- 这次更新属于什么优先级
- 接下来是否要尽快安排 render
4. scheduleUpdateOnFiber 为什么重要
React 不是哪儿改了状态就直接哪儿改 DOM。
它会把“这个 Fiber 上有更新”这件事统一交给调度入口处理。
可以把它理解成:
- 给当前更新打标签
- 把更新一路标记到根节点
- 决定这次任务接下来怎么被安排执行
精简后的思路示意:
js
function scheduleUpdateOnFiber(root, fiber, lane) {
markRootUpdated(root, lane);
ensureRootIsScheduled(root);
}这里最关键的不是函数名,而是这两个动作:
markRootUpdatedensureRootIsScheduled
也就是说,更新不是局部偷偷发生,而是会统一回到根节点调度。
5. lane 到底怎么理解
现代 React 源码里,一个很关键的词就是 lane。
可以把它理解成 一组表示更新优先级和调度类别的位标记。
你不用一开始就钻所有 lane 常量,但一定要先知道:
- React 不再只靠一个简单“同步 / 异步”二分法
- 不同更新可以有不同优先级
- React 会根据 lane 决定谁先算、谁可以稍后处理
例如概念上常见会区分:
- 用户输入这类更紧急更新
- 过渡性更新
- 空闲时再做的更新
所以 lane 更像“更新车道”,而不是普通数字序号。
5.1 lane 的能力边界
lane 很容易被误解成“新的优先级常量名”。
lane 真正负责的是:
- 给更新打类别和优先级标记
- 让 root 能知道当前有哪些更新待处理
- 让 React 在多类更新之间做选择
它不负责的是:
- 不自己执行调和
- 不自己做 DOM commit
- 不等于调度器本身
所以 lane 更像“更新标签体系”,而 scheduler 更像“根据这些标签安排执行的系统”。
6. scheduler 在 React 里扮演什么角色
很多人会把 scheduler 和 Fiber 混成一件事。
把这两层职责分开看:
| 能力 | 更偏解决什么 |
|---|---|
| Fiber | 更新工作如何表示和执行 |
| scheduler | 更新任务何时执行、谁优先执行 |
也就是说,Fiber 更偏“工作内容”,scheduler 更偏“工作安排”。
它的价值主要在这里:
- 避免所有更新都抢在同一时刻同步做完
- 让更重要的交互优先得到响应
- 给并发能力提供基础
6.1 scheduler 的完整释义和能力边界
scheduler 很容易被说成“React 的调度器”。
这个说法方向没错,但还需要补完整:
scheduler 更像 React 所依赖的一层通用任务调度能力。它负责根据优先级安排回调何时执行,让 React 不必把所有更新都一次性同步做完。`
它主要负责:
- 按优先级安排回调
- 让任务可分批、可打断、可延后
- 给并发相关能力提供执行基础
它不主要负责:
- 不理解 Fiber 节点内部结构
- 不自己生成 React 子树
- 不直接执行 DOM 操作
所以把这几层关系放回原位看:
- lane 告诉 React“这类更新是什么”
- scheduler 负责“这类更新什么时候跑”
- reconciler 负责“跑的时候怎么算”
7. 一份调度思路示意
下面这段代码仍然是保留主线后的阅读辅助版本:
js
function ensureRootIsScheduled(root) {
const nextLanes = getNextLanes(root);
const priority = lanesToSchedulerPriority(nextLanes);
scheduleCallback(priority, () => {
performConcurrentWorkOnRoot(root);
});
}这段示意最值得看的 3 件事是:
- 先从 root 里取“下一批最该处理的 lanes”
- 再把 React 自己的 lane 转成调度器优先级
- 最后交给
scheduleCallback(...)安排执行
所以调度并不是“某个 Hook 自己偷偷跑起来”,而是统一从 root 入口安排。
8. react-dom 在 commit 阶段做什么
到了 commit 阶段,React 已经知道哪些节点该插入、更新、删除。
这时 react-dom 真正重要的事情是:
- 创建 DOM 实例
- 更新属性
- 插入和删除 DOM 节点
- 处理文本节点
- 绑定或解绑事件
也就是说,react-reconciler 更偏“决定做什么”,react-dom 更偏“在浏览器里真正执行这些宿主操作”。
8.1 react-reconciler 和 react-dom 在 commit 上的边界
这两个包在 commit 这里最容易串线。
可以直接这样区分:
react-reconciler
更偏:
- 知道哪些 Fiber 有哪些 flags
- 知道当前该插入、更新还是删除
- 组织 commit 阶段的执行流程
react-dom
更偏:
- 真正创建 DOM 实例
- 真正更新属性和文本
- 真正把节点插入或移除
也就是说:
react-reconciler更像“决策和流程组织层”react-dom更像“浏览器宿主执行层”
如果不把这层边界讲清楚,就很容易误以为 commit 全都在 react-dom 或全都在 react-reconciler。
9. 客户端和服务端两条线怎么分
源码里其实会越来越明显地区分这两条线。
9.1 客户端
更常见的关注点是:
createRoothydrateRoot- commit 阶段的 DOM 操作
9.2 服务端
更常见的关注点是:
renderToString- 流式渲染
- 输出 HTML,而不是直接操作浏览器 DOM
也就是说,服务端这条线不是“换个地方跑 React DOM”,而是宿主目标本身就变了。
10. 常见相关源码文件
如果你继续顺着仓库看,通常会碰到这些位置:
| 文件 | 主要职责 |
|---|---|
packages/react-dom/src/client/ReactDOMRoot.js | createRoot / hydrateRoot 等客户端入口 |
packages/react-dom/src/client 下相关文件 | DOM 根节点、容器初始化 |
packages/scheduler/src/forks/Scheduler.js | 调度器主线 |
packages/react-reconciler/src/ReactFiberWorkLoop.js | render 与调度主循环连接点 |
建议先重点看:
- root 是怎么创建的
- 更新如何从 Fiber 回到 root 调度
- lane 怎样被挑选和调度
- commit 后半段怎么落到宿主环境
11. 这一层最常见的误区
11.1 以为 react-dom 负责全部 React 内核
不是。
它更偏宿主接入。
11.2 以为 scheduler 就是 Fiber
不是。
Fiber 更偏数据结构和执行模型,scheduler 更偏任务安排。
11.3 以为所有更新优先级都一样
现代 React 明显不是这样。
lane 和 scheduler 正是在解决这件事。
12. 一句话总结
react-dom 和 scheduler 这条源码主线,本质上是在做 把更新从 Fiber 节点统一提升到 root 调度,按 lane 和优先级安排执行,再由 react-dom 把最终 commit 结果真正落到浏览器 DOM 或服务端输出。