“你能讲讲虚拟 DOM 的原理吗?”
面试官盯着我,我条件反射地开始背八股文:“虚拟 DOM 是用 JS 对真实 DOM 的一种抽象,通过 diff 算法比较前后两个 vnode 树,找出变化后最小化更新 DOM……”
话音刚落,对方追问:“那你知道 Vue 是怎么判断两个节点相同的吗?diff 的时间复杂度是多少?为什么说虚拟 DOM 并不总是比手写 DOM 更快?”
我愣住了。
那一刻我意识到,仅靠八股文远远不够。想真正打动面试官,就必须跳出表面,深入本质。
所以这篇文章,我们就从这三个关键问题出发:
🧩 本文结构:
- DOM 工作原理
- 什么是虚拟 DOM
- 为什么需要虚拟 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 本质上是:
- JS 层面:解析字符串
- DOM 层面:创建所有 DOM 节点
而使用虚拟 DOM 也涉及到两个层面的计算:
- JS 层面:创建 JS 对象,即虚拟 DOM
- DOM 层面:根据 JS 对象创建对应的 DOM 节点
这里我们不用纠结“解析字符串”和“创建 JS 对象”谁更快,因为它们都发生在 JS 层,性能差距不大。真正拉开差距的,是 JS 层 vs DOM 层 的计算速度 —— 完全不在一个量级。
为什么我们用虚拟 DOM 取代了 innerHTML 的使用了呢?
2. 虚拟 DOM 的“加分项”:
- 初始渲染性能:与 innerHTML 基本持平
- 更新时性能提升巨大!
来看个例子 👇
<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 技术本质」系列内容。