Files
cs-note/hzh/REACT/虚拟 DOM 和 真实 DOM 有什么区别.md
T

6.1 KiB
Raw Blame History

tags, create time
tags create time
React
前端
虚拟DOM
DOM
2026-05-26 15:30

虚拟 DOM 和 真实 DOM 有什么区别

概述

虚拟 DOM(Virtual DOM)是 React 的"秘密武器"——它先用 JavaScript 对象在内存中画一张 UI 蓝图,然后通过一套高效的 Diff 算法找出改动最少的那部分,最后一次性刷到真实的页面上。这样既省去了频繁操作浏览器 DOM 的性能损耗,又保留了声明式编写 UI 的方便。

正文

先搞懂:什么是"真实 DOM"?

浏览器加载网页后,会把 HTML 文档转换成一棵节点树,这就是 DOM 树。每个 <div>、每段文字都是树上的一个节点。

<ul id="list">
  <li>苹果</li>
  <li>香蕉</li>
  <li>橙子</li>
</ul>

对应的 DOM 树结构:

graph TD
    UL["<ul id='list'>"] --> LI1["<li>苹果</li>"]
    UL --> LI2["<li>香蕉</li>"]
    UL --> LI3["<li>橙子</li>"]

问题来了:如果我想在"香蕉"前面加一个"草莓",传统写法是:

// ❌ 传统写法:手动改 DOM
const list = document.getElementById("list");
const strawberry = document.createElement("li");
strawberry.textContent = "草莓";
list.insertBefore(strawberry, list.children[1]);

看起来简单?但当页面有 100+ 个元素要同时更新时,这种逐条操作的方式会频繁触发浏览器的 重排(Reflow) 和 重绘(Repaint),体验明显卡顿。

[!QUESTION] 为什么多次修改 DOM 这么慢? 想象你在白板上画图:每擦一次就重新描一遍。而批量操作的思路是先画草稿纸,确认没问题了再一次性誊到白板上。

虚拟 DOM:DOM 树的"替身演员"

虚拟 DOM 不是新东西——它只是 用 JS 对象模拟 DOM 树的一份副本:

// JSX 写法(你写的)
const element = <ul id="list"><li>苹果</li><li>香蕉</li></ul>;

// React 内部实际生成的虚拟 DOM(纯 JS 对象)
const vdom = {
  type: "ul",
  props: { id: "list", children: [
    { type: "li", props: { children: ["苹果"] } },
    { type: "li", props: { children: ["香蕉"] } }
  ]}
};

这个对象 不占用任何浏览器资源,只在 JS 内存里跑。你可以把它理解为装修的 3D 效果图——你看的是图,但房子还没动。

真实 DOM 虚拟 DOM
是什么 浏览器的 DOM 节点对象 JS 普通对象
在哪运行 浏览器引擎(C++) JS 引擎(V8)
改动成本 高(触发布局计算) 低(只是对象赋值)
能跨平台吗 ❌ 只能浏览器 ✅ SSR、React Native

核心原理:Diff + Patch

虚拟 DOM 的威力不在"虚拟"本身,而在它的 对比机制。流程只有三步:

flowchart LR
    A["状态变化"] --> B["生成新的虚拟 DOM"]
    B --> C["新旧对比(Diff)"]
    C --> D["只更新变动的部分(Patch)"]
    style A fill:#e1f5fe
    style B fill:#fff3e0
    style C fill:#fce4ec
    style D fill:#e8f5e9

举个例子:添加一项水果

// 第 1 步:原始列表 —— 用户点了"添加草莓"
setState({ fruits: ["苹果", "香蕉", "橙子"] })
//   ↓
// 🍎  🍌  🍊

// 第 2 步:React 生成新的虚拟 DOM
setState({ fruits: ["苹果", "香蕉", "草莓", "橙子"] })
//   ↓
// 🍎  🍌  🍓  🍊

// 第 3 步:Diff 发现只有 1 处变化
// Patch 只需执行 1 次操作:<li>草莓</li> 插入到香蕉后面

对比两种方式的差异:

直接操作 DOM:
  ├── insertBefore()       ← 触发重排
  ├── 可能需要调整后续索引  ← 额外逻辑

React 的 VDOM:
  ├── 创建新 JS 对象        ← 纯内存操作,极快
  ├── Diff 比较两棵树       ← 内存中运算,也不影响渲染
  └── 批量应用变更          ← 浏览器只收到一条指令

[!TIP] CPU 换 GPU 思维 有人问:"Diff 本身也要算啊,不是多了一步吗?"——对,VDOM 确实多了一次 CPU 计算,但它把 N 次昂贵的 GPU(浏览器渲染管线的重排/重绘)合并成 1 次。用便宜的算力换取昂贵的渲染,这是现代前端的经典权衡。

Diff 是怎么做到这么快?

暴力对比两棵树的复杂度是 O(n³),不可接受。React 做了两个"偷懒"的设计把它们砍到 O(n):

  1. 同层对比:不同层的节点绝不拿来比,类型一变就直接销毁重建
  2. key 属性:列表项靠 key 来辨认身份,避免误判
假设列表从 [A, B, C] 变成 [B, A, C]:
  
暴力对比:逐个位置看,认为 3 个都变了 ❌
React Diff:通过 key 识别出 B 和 A 只是换了位置,只需交换 ✅

key 到底是什么?为什么非得加?

key 就是虚拟 DOM 对比时的 身份证。

// ✅ 正确:用不变的 ID 当 key
users.map(user => <li key={user.id}>{user.name}</li>)

// ❌ 错误:用 index 当 key
users.map((user, i) => <li key={i}>{user.name}</li>)

为什么不能用 index? 看这个场景:

初始列表:Alice(0), Bob(1), Carol(2)
删除 Alice 后:Bob 变成了 [0],Carol 变成了 [1]

用 index 当 key:
  Diff 看到 index=0 的值从 "Alice" 变成了 "Bob" 
  → 认为整个列表全变了,全部重建 ❌

用 user.id 当 key:
  Diff 看到 id=1 的 "Bob" 只是位置上移了一位
  → 只做移动,不重建 ✅

[!NOTE] 什么时候可以用 index? 列表完全不会增删排序、且项之间没有任何状态交互——几乎不存在这样的场景。安全起见,永远用稳定唯一 ID。

完整工作流程

sequenceDiagram
    participant U as 用户
    participant R as React
    participant V as 旧虚拟 DOM
    participant New as 新虚拟 DOM
    participant P as Diff 引擎
    participant D as 真实 DOM

    U->>R: 点击按钮 / setState
    R->>New: 用最新数据生成新虚拟 DOM
    R->>P: 传入旧虚拟 DOM 和新虚拟 DOM
    P->>P: 逐层对比,记录最小变更集
    P-->>R: 返回需要更新的位置
    R->>D: 批量写入变更
    D-->>U: 页面刷新

关联笔记