【重学 Vue:深入本质,脱离八股文,彻底搞定面试】:(一)虚拟DOM的本质

160 阅读7分钟

“你能讲讲虚拟 DOM 的原理吗?”

面试官盯着我,我条件反射地开始背八股文:“虚拟 DOM 是用 JS 对真实 DOM 的一种抽象,通过 diff 算法比较前后两个 vnode 树,找出变化后最小化更新 DOM……”

话音刚落,对方追问:“那你知道 Vue 是怎么判断两个节点相同的吗?diff 的时间复杂度是多少?为什么说虚拟 DOM 并不总是比手写 DOM 更快?”

我愣住了。

那一刻我意识到,仅靠八股文远远不够。想真正打动面试官,就必须跳出表面,深入本质。

所以这篇文章,我们就从这三个关键问题出发:


🧩 本文结构:

  1. DOM 工作原理
  2. 什么是虚拟 DOM
  3. 为什么需要虚拟 DOM

一、DOM 工作原理

💡 思考一个问题:我们写的是 JS 代码,浏览器内核是 C++ 写的,那 JS 是怎么让浏览器去创建一个 DOM 元素的?

const div = document.createElement("div");

这个过程依赖于一个桥梁:WebIDL(Web Interface Definition Language),即Web 接口定义语言

📌 WebIDL 的作用:

定义浏览器和 JS 引擎之间如何进行通信,换句话说,浏览器(C++)所提供的一些本地功能如何能够被 JS 调用。

假设有如下的 WebIDL 定义:一个名为 Document 的通信接口,该接口内部有一个 createElement 方法,用来创建 DOM 元素。

interface Document {
    Element createElement(DOMString localName);
};

浏览器开发者会用 C++ 实现它,定义具体如何来创建 DOM 元素

class Document {
public:
    Element* createElement(const std::string& tagName) {
        return new Element(tagName);
    }
};

接下来,WebIDL 编译器会自动生成一层绑定代码,将这个 C++ 方法暴露给 JS:

void Document_createElement(const v8::FunctionCallbackInfo<v8::Value>& args) {
    Document* document = Unwrap<Document>(args.Holder());
    std::string localName(*v8::String::Utf8Value(isolate, args[0]));
    Element* element = document->createElement(localName);
    args.GetReturnValue().Set(WrapElement(isolate, element));
}

最终注册到 JS 引擎:

void RegisterDocument(v8::Local<v8::Object> global, v8::Isolate* isolate) {
    tmpl->InstanceTemplate()->Set(isolate, "createElement", Document_createElement);
}

所以,当我们写:

document.createElement("div");

实际上是: JS 引擎识别创建 DOM 节点为 API 调用 → JS 引擎向浏览器底层(渲染引擎)发出调用请求 → 浏览器底层 执行 C++ 代码创建这个 DOM 元素 → 浏览器底层向最初的调用端返回一个结果。

✅ 总结:

我们说的 “真实 DOM”,其实是浏览器已经在底层用 C++ 创建的 对象结构,它不是 JS 世界的“原生对象”。


二、什么是虚拟 DOM?

📚 概念来源:

最早由 React 团队提出:

虚拟 DOM 是一种编程概念,在这个概念里, UI 的结构和状态以一种理想化的形式保存在内存中,作为真实 DOM 的“虚拟”映射。这种表示方式使得 UI 更新可以先在内存中进行计算,再通过高效的方式同步到实际的 DOM 上。

比如在 Vue 中,提供了一个名为 h 的函数,该函数的调用结果就是返回虚拟 DOM.。

简单示例如下:

// 父组件 App.vue

<template>
  <div class="app-container">
    <h1>这是App组件</h1>
    <Child name="李四" email="123@qq.com" />
    <component :is="vnode" />
  </div>
</template>
<script setup>
import { h } from 'vue'
import Child from '@/components/Child.vue'
const vnode = h(Child, {
  name: '李四',
  email: '123@qq.com'
})
console.log('vnode:', vnode)
</script>

// 子组件 Child.vue

<template>
  <div class="child-container">
    <h3>这是子组件</h3>
    <p>姓名:{{ name }}</p>
    <p>email:{{ email }}</p>
  </div>
</template>
<script setup>
defineProps({
  name: String,
  email: String
})
</script>

页面显示如下左图,vnode 控制台打印如下右图

🔍 输出的 vnode 是一个 JS 对象

✅ 总结:

虚拟 DOM 的本质就是普通的 JS 对象。


三、为什么需要虚拟 DOM?

1. 回顾早期操作 DOM 的方式(命令式):

<div id="app">
  <!-- 需求:往这个节点内部添加一些其他的节点 -->
</div>

如果是采用传统的操作 DOM 节点的方式:

// 获取app节点
var app = document.getElementById("app");
// 创建外层div
var messageDiv = document.createElement("div");
messageDiv.className = "message";
// 创建info子div
var infoDiv = document.createElement("div");
infoDiv.className = "info";
// 创建span元素并添加到infoDiv
var nameSpan = document.createElement("span");
nameSpan.textContent = "张三";
infoDiv.appendChild(nameSpan);
var dateSpan = document.createElement("span");
dateSpan.textContent = "2024.5.6";
infoDiv.appendChild(dateSpan);
// 将infoDiv添加到messageDiv
messageDiv.appendChild(infoDiv);
// 创建并添加<p>
var p = document.createElement("p");
p.textContent = "这是一堂讲解虚拟DOM的课";
messageDiv.appendChild(p);
// 创建btn子div
var btnDiv = document.createElement("div");
btnDiv.className = "btn";
// 创建a元素并添加到btnDiv
var removeBtn = document.createElement("a");
removeBtn.href = "#";
removeBtn.className = "removeBtn";
removeBtn.setAttribute("_id", "1");
removeBtn.textContent = "删除";
btnDiv.appendChild(removeBtn);
// 将btnDiv添加到messageDiv
messageDiv.appendChild(btnDiv);
// 将构建的messageDiv添加到app中
app.appendChild(messageDiv);

虽然这样写性能最好,但开发成本太高。为了开发体验,我们引入了 innerHTML :

var app = document.getElementById("app");

app.innerHTML += `
  <div class="message">
    <div class="info">
      <span>张三</span>
      <span>2024.5.6</span>
    </div>
    <p>这是一堂讲解虚拟DOM的课</p>
    <div class="btn">
      <a href="#" class="removeBtn" _id="1">删除</a>
    </div>
  </div>`;

但 innerHTML 本质上是:

  1. JS 层面:解析字符串
  2. DOM 层面:创建所有 DOM 节点

而使用虚拟 DOM 也涉及到两个层面的计算:

  1. JS 层面:创建 JS 对象,即虚拟 DOM
  2. DOM 层面:根据 JS 对象创建对应的 DOM 节点

这里我们不用纠结“解析字符串”和“创建 JS 对象”谁更快,因为它们都发生在 JS 层,性能差距不大。真正拉开差距的,是 JS 层 vs DOM 层 的计算速度 —— 完全不在一个量级。

为什么我们用虚拟 DOM 取代了 innerHTML 的使用了呢?


2. 虚拟 DOM 的“加分项”:

  1. 初始渲染性能:与 innerHTML 基本持平
  2. 更新时性能提升巨大

来看个例子 👇

<body>
  <button id="updateButton">更新内容</button>
  <div id="content"></div>
  <script src="script.js"></script>
</body>
// 通过 innerHTML 来更新 content 里面的内容
document.addEventListener("DOMContentLoaded", function () {
  const contentDiv = document.getElementById("content");
  const updateButton = document.getElementById("updateButton");
  updateButton.addEventListener("click", function () {
    const currentTime = new Date().toTimeString().split(" ")[0]; // 获取当前时间
    contentDiv.innerHTML = `
        <div class="message">
            <div class="info">
                <span>张三</span>
                <span>${currentTime}</span>
            </div>
            <p>这是一堂讲解虚拟DOM的课</p>
            <div class="btn">
                <a href="#" class="removeBtn" _id="1">删除</a>
            </div>
        </div>`;
  });
});

缺点: 每次点击都:

  • 销毁所有旧 DOM(DOM 层面)
  • 解析新的字符串(JS 层面)
  • 创建全新 DOM

而虚拟 DOM 是:

  • 先 diff 出变化的节点(JS 层面)
  • 再有选择地更新 DOM(DOM 层面)

再看一个虚拟 DOM 的性能对比实验 👇

console.time("JS对象创建");
for (let i = 0; i < 10_000_000; i++) {
  const node = { tag: 'div' };
}
console.timeEnd("JS对象创建"); // 几百毫秒

console.time("DOM节点创建");
for (let i = 0; i < 10_000_000; i++) {
  document.createElement("div");
}
console.timeEnd("DOM节点创建"); // 几千毫秒

👉 JS 层创建对象远快于 DOM 层!

✅ 总结:

使用虚拟 DOM 减少了 DOM 层面的计算,防止了组件在 **重渲染 **时导致的性能恶化。


四、虚拟 DOM 的更多优势

✅ 1. 跨平台能力

  • 虚拟 DOM 的本质,是在 UI 描述和底层渲染之间增加了一层 抽象层。通过引入这层中间结构,将上层逻辑与底层 DOM 操作进行了有效解耦。这种设计,实际上体现了经典的依赖倒置原则

高层模块(如业务逻辑)不应该依赖底层模块(如原生 DOM 操作)的具体实现,而是共同依赖于抽象(虚拟 DOM)。

  • 这一抽象层带来了非常重要的灵活性:开发者只需专注于使用虚拟 DOM 描述 UI 的结构,而底层如何将其渲染为真实界面,可以交由不同的渲染引擎实现,而不是局限于浏览器平台。

✅ 2. 框架架构更灵活

以 React 为例:

  • React 从 v15 升级到 v16 后,在架构层面发生了翻天覆地的变化:底层实现从 Stack 架构 升级为全新的 Fiber 架构,引入了更强的可控性与异步渲染能力。然而,这样一次庞大的内部重构,对开发者却几乎是无感知的。大多数开发者无需更改任何代码,仍然可以按照之前的方式开发组件。
  • 为什么可以做到这一点?关键就在于 虚拟 DOM 这个中间抽象层。React 将开发者的 JSX 代码编译为虚拟 DOM 结构,然后由框架内部对虚拟 DOM 进行调度和渲染。Fiber 架构的变动只影响虚拟 DOM 的底层处理逻辑,而不会影响开发者编写虚拟 DOM 的方式。

换句话说:虚拟 DOM 实现了开发逻辑和渲染机制的彻底解耦,这使得 React 可以在内部自由地升级架构,而不影响上层的使用体验。


五、新趋势:无虚拟 DOM 框架

有兴趣的同学可以一起了解一下,或许之后可以一起探讨:

  • Svelte / Solid.js:直接将模板编译为命令式 DOM 操作
  • Vue Vapor Mode(蒸汽模式):也在尝试无虚拟 DOM

GitHub 地址:github.com/vuejs/core-…

它们在性能上更强,因为没有中间层,没有 diff,直接操作 DOM


🧠 总结

虚拟 DOM 是在做一件事:

用 JS 层的 diff + 最小化更新,来减少不必要的 DOM 操作

它不一定更快,但它更“聪明”,特别在频繁更新的 UI 中,能显著提升性能和开发体验。

✔️ 真正的价值,在于“更新阶段”的优化
✔️ 更大的意义,是对底层 DOM 的抽象封装


📌 下一篇:【重学 Vue:深入本质,脱离八股文,彻底搞定面试】:(二)模板的本质

敬请期待~


如果你觉得这篇文章对你有帮助,欢迎关注 + 点赞 + 收藏,我会持续输出「Vue 技术本质」系列内容。