Skip to content

React 源码总览:包结构、运行链路与阅读顺序

很多人说想看 React 源码,但一打开仓库就容易先迷路。

原因很简单:React 不是一个单文件框架,而是一组包共同完成这件事:

  1. 组件和 Element 怎么表示
  2. 更新任务怎么组织
  3. 浏览器 DOM 怎么接入
  4. 服务端渲染怎么输出
  5. 调度器怎么安排优先级

所以看源码时,最重要的第一步不是记函数名,而是先建立 包结构 -> 职责 -> 运行链路 这张地图。

这篇文章重点讲:

  1. React 源码里最值得先认识的几个包
  2. 一次更新通常会穿过哪些核心文件
  3. 源码阅读时建议按什么顺序进入
  4. 现代 React 的源码主线为什么会落在 reactreact-reconcilerreact-domscheduler

1. 先建立一张源码地图

如果按职责而不是按仓库目录看,React 源码最值得先抓的是这几层:

包 / 目录更偏负责什么
packages/reactReact 对外 API、本体、Element、Hooks 入口
packages/react-reconcilerFiber、调和、render/commit 主线
packages/react-dom浏览器宿主接入、客户端挂载、水合、服务端输出
packages/scheduler调度与优先级相关基础能力
packages/shared各包共享的常量、工具和类型辅助

🌟 这一层的分工大致是:react 负责定义“React 是什么”,react-reconciler 负责“React 怎么更新”,react-dom 负责“React 怎么接到浏览器或服务端”,scheduler 负责“React 怎么安排工作”。


2. 为什么不能只看 react-dom

很多人第一次想找“React 怎么渲染到页面上”,就直奔 react-dom

这当然不算错,但如果只看这一层,很容易缺一大块上下文。

因为真正的更新主线通常是:

  1. react 先定义 JSX / Element / Hook 入口
  2. react-reconciler 负责把更新工作组织成 Fiber 树
  3. scheduler 负责调度优先级和执行时机
  4. react-dom 负责把最终结果提交到 DOM

也就是说,react-dom 更像宿主接入层,而不是全部 React 内核。


2.1 这几个核心包各自的能力边界

源码阅读时,最容易混的不是文件名,而是“这个包到底负责到哪”。

下面这张表更适合用来建立边界感:

主要负责什么不主要负责什么
react对外 API、Element 模型、Hook 入口、组件模型抽象不直接负责 DOM 操作,也不承载完整调和主循环
react-reconcilerFiber、render/commit 主线、diff、Hook 运行时、更新计算不直接操作浏览器 DOM,也不是最终调度器实现
react-dom浏览器宿主接入、root 创建、水合、DOM commit 落地、服务端输出接口不负责完整 Element 创建,也不独立决定全部更新优先级
scheduler任务调度、优先级回调安排、让工作可分批执行不知道组件长什么样,也不直接处理 Fiber 树结构
shared共享常量、符号、工具函数、内部通用辅助不承载某条业务主线或渲染主线

这张表最重要的作用,是避免把几层职责全部压成一句“React 在渲染页面”。

放回各自位置看,这几层分别是:

  1. react 定义的是模型入口
  2. react-reconciler 计算的是更新过程
  3. react-dom 落地的是宿主操作
  4. scheduler 安排的是执行时机

如果这 4 层不分开,后面看 createRootbeginWorkdispatchSetState 时就很容易串线。


3. 一次更新穿过哪些核心主线

可以把 React 的一次更新压缩成这条线:

mermaid
flowchart TD
    A[JSX / 组件执行] --> B[React Element]
    B --> C[调度更新]
    C --> D[Fiber 树构建与复用]
    D --> E[beginWork / completeWork]
    E --> F[commit]
    F --> G[DOM 更新]

    C --> H[scheduler]
    F --> I[react-dom]

如果把它映射到更常见的源码文件,大致会落在这些位置:

主线常见源码文件
Element 创建packages/react/src/jsx/ReactJSXElement.js
Fiber 节点定义packages/react-reconciler/src/ReactFiber.js
Hook 逻辑packages/react-reconciler/src/ReactFiberHooks.js
调和主循环packages/react-reconciler/src/ReactFiberWorkLoop.js
向下处理子树packages/react-reconciler/src/ReactFiberBeginWork.js
向上收集副作用packages/react-reconciler/src/ReactFiberCompleteWork.js
commit 阶段packages/react-reconciler/src/ReactFiberCommitWork.js
客户端根节点packages/react-dom/src/client/ReactDOMRoot.js

这里最重要的不是强行背文件名,而是先知道 React 不是一个函数做完全部工作,而是被拆成了 Element、Fiber、Hook、调度、commit、宿主接入这几层。

3.1 看 4 段最值得建立感觉的源码骨架

上面那张图解决的是“地图”问题,但很多人看完还是会觉得不够落地,因为还没真正碰到源码形状。

所以在进入后面几篇专题之前,更适合看 4 段精简后的核心片段。它们不是 React 仓库里的逐字符原文,而是保留主线后的阅读辅助版本,重点是让你抓住:

  1. React Element 到底是什么对象
  2. Fiber work loop 到底怎么推进
  3. Hook 为什么必须按顺序挂到 Fiber 上
  4. 更新为什么总会回到 root 调度

3.1.1 React Element:看“JSX 最后产出什么”

js
function ReactElement(type, key, ref, props) {
  return {
    $$typeof: REACT_ELEMENT_TYPE, // 标记这是不是 React Element
    type, // 可能是 "div",也可能是函数组件
    key, // 同层节点身份标识
    ref, // 命令式引用入口
    props, // 普通输入数据和 children
    _owner: null
  };
}

这段骨架最值得先记住的是:JSX 最终先变成一份描述对象,而不是直接变成 DOM。

也就是说,React 前面先解决的是“要渲染什么”,后面才继续解决“怎么更新它”和“怎么落到宿主环境”。

3.1.2 Fiber work loop:看“更新是怎么一步步跑起来的”

js
function workLoop() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork) {
  const next = beginWork(unitOfWork); // 向下处理当前节点和子树
  unitOfWork.memoizedProps = unitOfWork.pendingProps;

  if (next === null) {
    completeUnitOfWork(unitOfWork); // 子树处理完了,开始向上收口
  } else {
    workInProgress = next;
  }
}

这段骨架的价值在于,它把 render 阶段压缩成了一条非常清楚的线:

  1. beginWork 向下展开
  2. 子节点走完后再向上 complete
  3. 整个 render 阶段本质上是在反复推进 workInProgress

🌟 所以 Fiber 主线不是“神秘黑盒”,而是 沿着 Fiber 树不断向下处理、向上收口的一套工作循环。

3.1.3 Hook 挂载:看“为什么调用顺序不能乱”

js
function mountWorkInProgressHook() {
  const hook = {
    memoizedState: null, // 当前 Hook 记住的状态
    queue: null, // 这个 Hook 对应的更新队列
    next: null // 指向下一个 Hook,多个 Hook 会串成链表
  };

  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = hook; // 第一个 Hook 挂到 Fiber 上
    workInProgressHook = hook;
  } else {
    workInProgressHook.next = hook; // 后续 Hook 接到链表后面
    workInProgressHook = hook;
  }

  return hook;
}

这段骨架最重要的不是记字段,而是看懂 Hook 是按调用顺序一个个挂到当前 Fiber 上的。

这也正是为什么:

  1. 不能把 Hook 放进条件分支
  2. 不能有时多调一个、有时少调一个

因为 React 不是按名字找 Hook,而是按调用顺序沿着这条链表往后取。

3.1.4 调度入口:看“为什么更新最后都会回到 root”

js
function scheduleUpdateOnFiber(root, fiber, lane) {
  markRootUpdated(root, lane); // 把这次更新标记到 root
  ensureRootIsScheduled(root); // 再决定这批更新接下来怎么被调度执行
}

function ensureRootIsScheduled(root) {
  const nextLanes = getNextLanes(root); // 选出下一批最该处理的更新
  const priority = lanesToSchedulerPriority(nextLanes);

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

这段骨架帮助你先建立一个特别重要的认识:React 不是哪个组件 setState 了,就只在那个组件局部偷偷改一改。

更常见的主线其实是:

  1. 当前 Fiber 上产生更新
  2. 更新一路归并到 root
  3. root 再根据 lane 和优先级决定什么时候进入下一轮 render

所以源码里很多看起来“绕回根节点”的动作,并不是多余,而是 React 统一调度的关键。

3.2 看完这 4 段后,建议带着这几个问题继续读

如果上面 4 段骨架已经有感觉了,后面再进入专题时,最值得继续追的是:

  1. React Element 怎么从 jsx / createElement 真正构造出来
  2. beginWorkcompleteWork 分别在不同 Fiber 类型上做了什么
  3. useState 的更新队列怎么和 Hook 链表连起来
  4. lane 怎么把“更新类别”和“调度优先级”串到一起

这样后面再看具体源码文件,就不容易陷入“函数名很多,但不知道它们到底接在哪条线上”的状态。


4. 看源码时最容易混的 3 组概念

4.1 reactreact-dom

  • react 更偏 API 和模型
  • react-dom 更偏浏览器 / 服务端宿主接入

4.2 Fiber 和 scheduler

  • Fiber 更偏“更新工作的数据结构和执行模型”
  • scheduler 更偏“任务怎么调度、谁先做”

4.3 render 和 commit

  • render 阶段更偏“计算下一棵树”
  • commit 阶段更偏“真正落地变更”

这 3 组不先区分清楚,源码很容易看成一团。


4.1 一次更新里,哪些能力分别归谁

如果把一次状态更新拆成更细一点的责任分工,可以这样看:

  1. 组件执行、JSX 产出 Element:主要属于 react
  2. 把更新组织进 Fiber、比较差异、收集副作用:主要属于 react-reconciler
  3. 安排任务优先级和执行窗口:主要依赖 scheduler
  4. 把最终变更真正落到浏览器 DOM:主要属于 react-dom

这条分工很重要,因为它能回答几个高频误区:

  • 为什么 react 包本身并不直接改 DOM
    因为 DOM 是宿主环境,归 react-dom 处理。

  • 为什么 scheduler 不能单独解释 React 更新
    因为它解决的是“什么时候做”,不是“具体怎么算”。

  • 为什么 react-dom 不是完整内核
    因为它承接的是宿主接入,而不是整条调和主线。

源码阅读时如果看到一个函数,先问自己:

  1. 它是在描述 UI 模型
  2. 还是在计算 Fiber 更新
  3. 还是在调度任务
  4. 还是在落地宿主操作

这样比直接背函数名更稳。


5. 一份更适合入门的阅读顺序

如果你是第一次系统读 React 源码,更阅读顺序:,而不是直接冲最深的 work loop。

5.1 看 Element

先理解:

  1. JSX 最终变成什么
  2. React.createElement / jsx 最终返回什么对象

这样后面再看 Fiber,你才知道“树上的节点一开始从哪里来”。

5.2 再看 Fiber 数据结构

先弄清:

  1. Fiber 节点长什么样
  2. 它和 React Element 的关系
  3. child / sibling / return 为什么是这套链表式结构

5.3 再看 render / complete / commit

这时再看:

  1. beginWork
  2. completeWork
  3. commitRoot

就不会只剩名词。

5.4 再看 Hooks

因为 Hook 的挂载和更新,本质上也建立在当前 Fiber 和渲染过程之上。

5.5 最后看 react-domscheduler

这时再去看:

  1. createRoot
  2. hydrateRoot
  3. 优先级和调度

层次会清楚很多。


6. 一个“先别急着钻太深”的判断标准

如果你现在还不能稳定回答这几个问题,就不建议一开始就钻进最深层函数:

  1. JSX 最后返回的是 DOM 还是对象
  2. React Element 和 Fiber 是什么关系
  3. render 阶段和 commit 阶段在做什么
  4. 为什么 Hook 必须保持调用顺序稳定
  5. createRoothydrateRoot 分别接在哪条链路上

这些主线没串起来,源码越看越碎。


7. 接下来建议按这几篇继续读

如果顺着当前 React 源码线往下看,比较自然的顺序是:

  1. ReactElement、JSX 与 createElement 源码主线
  2. Fiber 数据结构与 render/commit 源码主线
  3. Hooks 源码:useState、useEffect 与 Hook 链表
  4. react-dom、scheduler 与更新调度源码主线

8. 一句话总结

React 源码最值得先建立的认识是 它不是一个“渲染函数”,而是由 react、react-reconciler、react-dom、scheduler 这些包共同组成的一套组件模型、调和模型、宿主接入和调度系统。

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