在前端开发领域,性能优化始终是重中之重。虚拟 DOM(Virtual DOM)作为现代前端框架的核心技术之一,像 Vue,深刻地影响着我们构建用户界面的方式。那么,虚拟 DOM 究竟是什么?它在 Vue 中又有着怎样的作用呢?
一、什么是虚拟 DOM
1. 概念
虚拟 DOM 是真实 DOM 在 JavaScript 中的抽象表示,是一棵轻量级的 JavaScript 对象树,包含创建真实 DOM 节点所需的信息,如标签名、属性、子节点等 。在 Vue 中,我们编写的模板语法最终会被编译成虚拟 DOM。例如,有如下模板
<template>
<div>
<span>hello world</span>
</div>
</template>
// 上面的模板最终会被编译成下面的代码
const obj = {
tag: "div",
children: [{ tag: "span", children: "hello world" }],
};
// ☝️ 这就是虚拟 dom
// tag 为标签名 children 如果是数组就是子节点,如果是字符串就是内容
二、 为什么要用虚拟 Dom?作用是什么?
知道了虚拟 Dom 是什么东西,那么为什么我们要用他呢?我们直接用真实的 Dom,不是可以省去把模板转为虚拟 Dom 和把虚拟 Dom 渲染到页面两个步骤吗?其实用虚拟 Dom,一共有两个原因
1.减少心智负担
我们使用真实 Dom 的时候,我们应该怎么用?
const app = document.querySelector("#app");
app.innerText = 'hello world';
如果每一行都要那么写,那废了,手都要写断。所以虽然真实 Dom 性能好,但是很麻烦。
虚拟 Dom 使你可以直接在模板里书写变量,使你代码写得更舒服
2.减少频繁的操作Dom,优化性能
为什么要减少频繁操作 Dom?可以看下面的数据对比 👇
// 使用Console API进行性能标记
console.time("obj dom");
const app = [];
for (let i = 0; i < 10000; i++) {
const div = { tag: "div" };
app.push(div);
}
console.timeEnd("obj dom"); // 输出执行时间
console.time("real dom");
const app2 = document.querySelector("#app");
for (let i = 0; i < 10000; i++) {
const div = document.createElement("div");
app2.appendChild(div);
}
console.timeEnd("real dom"); // 输出执行时间
// obj dom: 0.22998046875 ms
// real dom: 6.163818359375 ms
可以看到,创建真实 Dom 会比创建 Js 对象慢很多,操作也是如此。假如后台返回了数据,我们再像上面一行一行的全量更新 Dom,因为我们不知道哪些 Dom 有变化,哪些没变化,所以我们全部都得更新。或者因为短时间多次修改要多次更新 Dom,这样就会浪费较多的时间。基于这一点,Diff 算法就诞生了。
当把模板转换为虚拟 Dom,我们就能使用 Diff 算法,最大程度的减少操作 Dom 的次数和范围 。
三、Vue 中虚拟 DOM 源码逻辑模拟
下面通过一些简单的逻辑,梳理一下虚拟 Dom 的具体使用
<template>
<div id="app">
<p>{{ message }}</p>
<button @click="changeMessage">Change Message</button>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello, Vue Virtual DOM'
};
},
methods: {
changeMessage() {
this.message = 'New Message';
}
}
};
</script>
在这个示例中,当点击按钮触发 changeMessage 方法时,message 数据发生变化。Vue 内部会:
- 生成新的虚拟DOM :根据新的 message 值生成新的虚拟 DOM 树。
- Diff算法对比 :将新的虚拟 DOM 树与旧的进行对比,计算出差异。
- 更新真实DOM :仅将差异部分应用到真实 DOM 上,更新
标签的文本内容,而不会重新渲染整个
。
知道了什么是虚拟 Dom,以及虚拟 Dom 的作用是什么,那我们可以最后来扩展一下。Vue 是如何把模板代码转换为虚拟 Dom 的。
1. 模板解析
Vue 首先会对用户编写的模板字符串进行解析,将其转换为抽象语法树(AST)。这一步骤借助了 parse 函数
function parse(template) {
let index = 0;
let root = null;
let currentParent = null;
const stack = [];
// 解析开始标签
function parseStartTag() {
const start = template.indexOf("<", index);
if (start === -1) return null;
index = start + 1;
const end = template.indexOf(">", index);
const tagName = template.slice(index, end);
index = end + 1;
const node = {
type: 1,
tag: tagName,
children: [],
};
return node;
}
// 解析文本内容
function parseText() {
const start = index;
const end = template.indexOf("<", index);
if (end === -1) {
const text = template.slice(index);
index = template.length;
return text.trim() ? { type: 3, text } : null;
}
const text = template.slice(index, end);
index = end;
return text.trim() ? { type: 3, text } : null;
}
while (index < template.length) {
const textNode = parseText();
if (textNode) {
if (currentParent) {
currentParent.children.push(textNode);
}
continue;
}
const startTag = parseStartTag();
if (startTag) {
if (!root) {
root = startTag;
}
if (currentParent) {
currentParent.children.push(startTag);
}
stack.push(startTag);
currentParent = startTag;
}
}
return root;
}
// 测试示例
const template = "<div>Hello, World</div>";
const ast = parse(template);
console.log(ast);
parse 函数会对模板字符串进行词法分析和语法分析。它会扫描模板字符串,识别出其中的标签、属性、文本等元素,并将它们转换为AST节点 。函数省略了很多逻辑,真实的逻辑需要考虑更多的东西,了解个基本功能就行。
2. 生成抽象语法树(AST)
解析完成后,会得到一个抽象语法树(AST),这是一个树形结构,它包含了模板的所有信息,如标签名、属性、子节点等。
{
"type": 1,
"tag": "div",
"attrsList": [],
"attrsMap": {},
"children": [
{
"type": 3,
"text": "Hello, World"
}
]
}
- type:表示节点的类型,1 代表元素节点,2 代表带有表达式的文本节点,3 代表普通文本节点。
- tag:标签名,这里是 div。
- attrsList 和 attrsMap :存储节点的属性信息。
- children:存储子节点信息,这里包含一个带有表达式的文本节点。
3. 将 AST 转换为渲染函数
Vue 会将生成的 AST 转换为渲染函数。这一步骤由 generate 函数完成
import { generate } from 'vue-template-compiler';
const code = generate(ast);
// code
// {
// render: 'with(this){return _c('div',[_v("Hello, World")])}',
// staticRenderFns: []
// }
// _c 是 Vue 内部的创建元素函数,用于创建 VNode(虚拟节点)。
// _v 是 Vue 内部的创建文本节点函数,用于创建文本 VNode。
const renderFunction = new Function(`with(this){return ${code.render}}`);
// renderFunction 结果示例:
// {
// "tag": "div",
// "props": null,
// "children": [
// "Hello, World"
// ]
// }
- generate 函数会根据 AST 生成渲染函数的代码字符串。
- new Function 会将代码字符串转换为可执行的 JavaScript 函数,这个函数就是渲染函数。渲染函数会返回虚拟 Dom。
generate 涉及了很多东西,比较复杂,这里就不贴了,感兴趣的可以去 Vue 源码 src/compiler/codegen/index.js 目录看一看,保证你看了不想看。这里我们只要知道他会把一个 AST 解析成虚拟 Dom 即可。
4. 最后再将虚拟 Dom 渲染成真实 Dom
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// 虚拟 DOM 对象
const vnode = {
tag: "div",
props: null,
children: ["Hello, World"],
};
// 将虚拟 DOM 转换为真实 DOM 的函数
function renderVNode(vnode) {
// 如果 vnode 是字符串,创建文本节点
if (typeof vnode === "string") {
return document.createTextNode(vnode);
}
// 创建元素节点
const el = document.createElement(vnode.tag);
// 设置属性
if (vnode.props) {
for (const key in vnode.props) {
el.setAttribute(key, vnode.props[key]);
}
}
// 处理子节点
if (vnode.children) {
vnode.children.forEach((child) => {
const childEl = renderVNode(child);
el.appendChild(childEl);
});
}
return el;
}
// 调用 renderVNode 函数将虚拟 DOM 转换为真实 DOM
const realDom = renderVNode(vnode);
// 将真实 DOM 添加到页面上
document.body.appendChild(realDom);
</script>
</body>
</html>
老规矩,示例代码。实际情况要更加复杂。
四、总结
虚拟 DOM 通过高效的 Diff 算法和最小化 DOM 操作,极大地提升了页面性能和开发效率。未来将会着重解析剩下的部件,慢慢完善。