陌上人如玉
公子世无双

4.1 虚拟 DOM 与 React Element

4.1 虚拟 DOM 与 React Element

一句话:虚拟 DOM 是「用普通 JS 对象描述 UI 结构」的一层抽象;React 通过比较新旧两棵对象树来算出最小 DOM 操作——它的价值是「可预测的更新模型 + 跨平台能力」,而不是「比直接操作 DOM 更快」。

一、虚拟 DOM 是什么

// 你写的 JSX
const ui = (
  <ul className="list">
    <li>第一项</li>
    <li>第二项</li>
  </ul>
);

// 编译后(简化)——就是普通的对象
const ui = {
  type: 'ul',
  props: {
    className: 'list',
    children: [
      { type: 'li', props: { children: '第一项' } },
      { type: 'li', props: { children: '第二项' } },
    ],
  },
};
虚拟 DOM(Virtual DOM)的准确定义:
  一棵用 JavaScript 对象表示的「UI 描述树」。
  它不是 DOM,也没有浏览器资源——只是数据。

React 的工作流程:
  ① 组件返回元素树(虚拟 DOM)
  ② React 把新的元素树与上一次的对比(diff / 协调)
  ③ 算出「需要哪些 DOM 操作」
  ④ 批量把这些操作应用到真实 DOM(commit)
三个容易混淆的概念:

① React Element(元素)
   不可变的对象,描述「某个时刻的 UI 长什么样」
   { $$typeof, type, props, key, ref }

② Fiber(Fiber 节点)
   一个「可持久化的工作单元」,除了元素信息外还包含:
   父子兄弟指针、备用节点(alternate)、优先级(lanes)、副作用标记(flags)
   它是 React 内部用来「可中断地工作」的数据结构

③ 真实 DOM
   浏览器对象,有布局与内存开销

关系:Element 是「说明书」,Fiber 是「施工计划」,DOM 是「建成的房子」

二、为什么需要虚拟 DOM(三个真实理由)

1) 声明式编程的前提

// 命令式:你要自己算「该改什么」
function updateUI(count) {
  const el = document.getElementById('count');
  if (count > 0) {
    el.textContent = String(count);
    el.classList.add('positive');
  } else {
    el.classList.remove('positive');
  }
}

// 声明式:你只说「应该长什么样」,React 负责算差异
function Counter({ count }) {
  return <span className={count > 0 ? 'positive' : ''}>{count}</span>;
}
价值:把「状态 → UI 的映射」写成纯函数,**不需要维护「从状态到 DOM 操作」的命令式代码**。
这是虚拟 DOM 最核心的价值——「可预测的更新模型」。
(Vue 的模板、Svelte 的编译时方案也是在解决这个问题,只是路径不同。)

2) 批量更新与减少不必要的 DOM 操作

// 一次渲染中可能有大量状态变化,但只有少数会影响 DOM
function Form() {
  const [a, setA] = useState('');
  const [b, setB] = useState('');
  // 输入 a 时,b 对应的 DOM 完全不需要变
  return (
    <>
      <input value={a} onChange={(e) => setA(e.target.value)} />
      <input value={b} onChange={(e) => setB(e.target.value)} />
    </>
  );
}
虚拟 DOM 让 React 可以在「计算完所有差异后一次性应用」:
① 把更新积累起来(批处理)
② 计算最小差异集合
③ 统一应用到 DOM(减少 layout thrashing)

⚠️ 注意:这里的收益是「减少开发者手写命令式代码的出错概率」与「集中调度更新」,
   而不是「算法层面比手写 DOM 操作更快」。

3) 跨平台能力

同一棵「虚拟 DOM」可以渲染到不同目标:
  · react-dom          → 浏览器 DOM
  · react-native       → 原生 iOS/Android 组件
  · react-three-fiber  → WebGL
  · react-pdf          → PDF
  · ink                → 终端
  · react-canvas       → Canvas

实现方式就是「替换渲染器」——组件的写法和状态管理完全复用。
这是 JSX 作为「UI 描述语言」带来的最大红利。

三、「虚拟 DOM 比原生 DOM 快」是误解

准确的说法是:

❌ 「虚拟 DOM 比直接操作 DOM 快」
→ 恰恰相反:虚拟 DOM 是在「真实的 DOM 操作之上」又加了一层计算。
   如果你手写「命令式的最小 DOM 操作」,一定会比「先 diff 再操作」快。

✅ 「虚拟 DOM 在『不手动优化』的前提下,避免了很多常见的性能陷阱」
→ 它让「整体性能的下限」变高:
   · 不用关心「该更新哪个节点」(避免漏更新、重复更新)
   · 天然批处理(避免频繁触发同步布局)
   · 容易做「跳过不需要更新的子树」的优化(memo)

✅ 「虚拟 DOM 的代价换来了:声明式、可预测、跨平台」
→ 这是划算的交易,但不是「绝对更快」。
说法 是否成立
虚拟 DOM 一定比手写 DOM 操作快 ❌ 错误
虚拟 DOM 让「不优化」时的性能不至于太差 ✅
虚拟 DOM 提供了声明式编程与跨平台能力 ✅ 主要价值
Svelte 这种「编译时方案」可以比虚拟 DOM 快 ✅(因为它把 diff 生成为直接 DOM 操作)
为什么 Svelte / Solid 能更快?
  它们在【编译时】就知道「哪个状态影响哪个 DOM 节点」,
  于是直接生成「更新那个节点的代码」,跳过了运行时的 diff。
  React 因为「每次渲染都重新执行组件函数」的模型,
  必须在运行时比较(所以又有了 React Compiler 来在编译时做记忆化)。

四、虚拟 DOM 的代价

① 内存开销
   每个元素/组件都要创建一个对象(Element + Fiber),大树会有可观的内存占用

② 计算开销
   每次渲染都要「遍历 + 比较」整棵(未被 memo 跳过的)子树

③ 首次渲染
   必须「创建对象 → 构建 Fiber 树 → 生成 DOM」,比直接写 HTML 慢

④ 心智模型
   开发者需要理解「渲染是重新执行函数」「引用稳定」等概念才能优化
React 在这些代价上做的优化:
① Fiber + 时间切片 → 长渲染不阻塞主线程(见 4.2)
② memo / useMemo → 跳过不需要更新的子树
③ 批处理 → 合并多次更新为一次渲染
④ React Compiler → 编译时自动插入记忆化,减少运行时比较
⑤ 编译时标记(如 static flags)→ 复用「不会变的子树」

五、React Element 的结构回顾

// 一个 Element 的真实结构(简化)
{
  $$typeof: Symbol(react.element),    // 防伪造(JSON 无法构造 Symbol)
  type: 'ul',                         // 字符串(宿主组件)或函数/类(自定义组件)
  key: null,                          // 列表中的身份标识
  ref: null,
  props: {
    className: 'list',
    children: [/* 子元素 */],
  },
  _owner: null,                       // 调试用(谁创建了它)
}
type 的值 含义 渲染方式
字符串 'div' 宿主组件(Host Component) 创建/更新真实 DOM 元素
函数 函数组件 调用函数得到元素树
类 类组件 实例化并调用 render
Fragment / Symbol 特殊类型 不产生 DOM 或特殊处理
Symbol(react.portal) Portal 渲染到指定容器

六、常见坑

// ① 以为「虚拟 DOM 就是 DOM 的副本」
// → 它只是「描述结构的数据」,不含样式、布局、事件

// ② 以为「虚拟 DOM 让操作 DOM 变快了」
// → 它是额外的抽象层;快是因为「批量 + 跳过」

// ③ 在渲染中做副作用(以为「渲染只是生成对象」很便宜)
function Bad() {
  fetch('/api');            // ❌ 渲染函数应该纯
  return <div />;
}

// ④ 直接改 state 后期待 UI 更新
user.name = 'x';            // ❌ 引用没变 → diff 认为「没变化」

// ⑤ 用 index 作 key(破坏了 diff 正确复用节点的基础)
// ⑥ 认为「元素是不可变的,所以不能更新 UI」
// → 更新是「创建新元素树」,由 React 比较(diff)后应用到 DOM

// ⑦ 手动操作 DOM 与 React 打架
ref.current.innerHTML = '<div>...</div>';    // ⚠️ React 不知道你改了它
// → 下次渲染可能被覆盖,或引发 hydration mismatch

// ⑧ 认为「跨平台能力是虚拟 DOM 的副产品,不用特意设计」
// → 它要求渲染器与「宿主无关」的抽象(React 用 host config 实现)

// ⑨ 认为「用了虚拟 DOM 就不需要关心 DOM 性能了」
// → 大量 DOM 节点、频繁布局抖动依然会卡(见 05 章)
// ⑩ 同时使用多个 React 渲染器版本(会报 Invalid hook call)

七、面试延伸

  1. 「什么是虚拟 DOM?」
    虚拟 DOM 是「用 JavaScript 对象描述 UI 结构的树」——它不是 DOM、也没有浏览器资源,只是数据。React 让组件返回这样一棵树,然后把它与上一次的树做差异比较(diff/协调),算出「需要哪些真实的 DOM 操作」,再批量应用到浏览器。它的核心价值是「让 UI 成为状态的纯函数映射」,实现声明式编程。

  2. 「虚拟 DOM 比直接操作 DOM 快吗?」
    不是。虚拟 DOM 是在真实 DOM 操作之上又加了一层计算(创建对象、diff、遍历),所以「手写最优的命令式 DOM 操作」一定比它快。它的真实价值是:① 声明式——不需要手写「该改哪个节点」的命令式代码;② 减少低级错误——不会漏更新或重复更新;③ 批处理与跳过——把多次更新合并、跳过不需要更新的子树;④ 跨平台。用一句话概括是「用一点性能换取可预测性与开发效率」。

  3. 「为什么 React 需要虚拟 DOM?」
    三个理由:① 声明式的前提——要有「新旧 UI 描述」才能比较,从而让开发者只描述结果;② 跨平台——同一棵虚拟 DOM 可以渲染到 DOM、Native、Canvas、PDF 等(替换渲染器即可);③ 统一调度——有了中间层,React 才能做批处理、优先级调度、可中断渲染(这也是 Fiber 的前提)。

  4. 「React Element、Fiber、真实 DOM 有什么区别?」
    Element:不可变的对象,描述「某一时刻 UI 长什么样」(含 type / props / key / $$typeof);Fiber:React 内部的「工作单元」,在 Element 信息之外还带「父子兄弟指针、alternate(另一棵树的对应节点)、lanes(优先级)、flags(副作用标记)」,它是「可中断渲染」的数据结构;真实 DOM:浏览器的对象,有布局与内存开销。打个比方:Element 是说明书,Fiber 是施工计划,DOM 是建成的房子。

  5. 「虚拟 DOM 的代价是什么?」
    四个:① 内存——每个元素/组件节点都要创建对象;② 计算——每次渲染都要遍历比较(未被 memo 跳过的子树);③ 首次渲染更慢——需要「创建对象 → 建 Fiber 树 → 生成 DOM」,比直接输出 HTML 慢;④ 心智成本——开发者要理解「渲染即重新执行函数」「引用稳定」等概念。React 用 Fiber 时间切片、memo、批处理、React Compiler 等手段来抵消这些代价。

  6. 「Svelte / Solid 为什么可能更快?」
    因为它们走的是「编译时方案」:在编译阶段就知道「哪个状态变量影响哪一处 DOM」,于是直接把更新逻辑编译成「操作那个具体节点的代码」,运行时不需要 diff。React 选择的是「运行时方案」——因为「每次渲染都重新执行组件函数」的模型更灵活(组件是普通 JS),但代价就是运行时要做比较与记忆化。两者是不同的取舍,不是绝对的优劣。

  7. 「什么情况下不该用虚拟 DOM?」
    当「更新模式极其固定且性能极端敏感」时——比如静态内容(直接输出 HTML 更快)、极高频的动画/可视化(用 Canvas/WebGL 直接绘制更好)、需要处理上万 DOM 节点的场景(虚拟滚动或 Canvas 更合适)。React 生态在这些场景里通常的做法是「绕开虚拟 DOM」:用 React.memo 隔离、用 ref 直接操作、或把绘制交给 Canvas。

  8. 「虚拟 DOM 和模板编译(Vue 的模板)有什么区别?」
    两者都在解决「声明式 UI + 高效更新」,区别在中间表示与优化时机:① React 用 JSX + 运行时——JSX 编译为函数调用,结构信息在运行时由 React 通过 diff 得出;② Vue 用模板 + 编译时优化——模板在编译时能分析出「静态节点、动态绑定、依赖关系」,生成带优化提示的渲染函数(如 PatchFlags),从而在运行时跳过大部分比较(Vue 3 还有编译时生成的 Block Tree)。所以 Vue 在「模板场景」下往往有更少的运行时开销,而 React 的表达力(JS 的完整能力)更强。

一句话速记

虚拟 DOM 是「用 JS 对象描述 UI 的树」(Element 是说明书、Fiber 是施工计划、DOM 是房子);它的价值是「声明式 + 可预测的更新模型 + 批处理 + 跨平台」,而不是「比原生 DOM 快」(它是额外一层计算,手写最优 DOM 操作一定更快);代价是内存 + 遍历比较 + 首屏更慢,React 用 Fiber 时间切片 / memo / 批处理 / Compiler 抵消;Element 是不可变对象($$typeof 用 Symbol 防伪造),更新 = 创建新树 + diff + 应用到 DOM。

赞(0) 打赏
未经允许不得转载:陌上寒 » 4.1 虚拟 DOM 与 React Element

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫