虚拟DOM(VNode) 和真实DOM的渲染效率问题一直是面试中高频、高难度话题。
昨天有个同学在面试中就被问到了这个问题。
面试官提问说:“虚拟DOM(VNode)渲染效率一定比 真实DOM 快吗?你是如何得到的这个结论?你的依赖数据是什么?”
好吧… 大厂的面试就是不一样,光靠背八股文肯定是混不过去了。
那么今天,就让咱们带着这三个问题,一起来看看 虚拟DOM(VNode) 和 真实DOM 的真实渲染效率,以及测试、依赖数据!
1、测试逻辑
在去讲解测试逻辑之前,我们先来明确一下什么叫做 虚拟DOM 和 真实DOM。
虚拟DOM
所谓虚拟DOM指的是:通过 JS 对象来模拟 DOM 结构。
所以我们可以简单的把 虚拟DOM 理解为 JS 对象。只不过这个 JS 对象比较复杂一些而已。
真实DOM
所谓真实DOM就是:通过 JS 获取到的 html 标签元素。
比如,我们通过 document.querySelect('#app')
拿到的这个元素就被成为 DOM 元素,也就是 真实DOM。
测试逻辑
那么明确好了两个基本概念之后,接下来咱们就说下测试方案。整个测试方案分成 4 个部分:
- DOM 操作 和 JS 操作 对比:来看下 DOM 操作 和 JS 操作的性能耗时差异
- DOM 操作 和 VNode 操作 对比:来看下 涉及到 VNode 操作时,性能差异
- DOM 操作 和 复杂的VNode 操作 对比:来看下 当 VNode 变得复杂时,性能消耗增加了多少
- 复杂DOM 操作 与 复杂VNode 操作 对比:来看下当 DOM 操作都变得复杂时,性能消耗差异
明确好了以上内容之后,就让咱们开始吧~~
2、DOM 操作与数据操作对比
<body>
<div id="app"></div>
<script>
const COUNT = 10000
console.time('dom 耗时')
for (let i = 0; i < COUNT; i++) {
const ele = document.createElement('div')
ele.innerHTML = 'hello bunian'
document.querySelector('#app').appendChild(ele)
}
console.timeEnd('dom 耗时')
console.time('js 耗时')
const vNodes = []
for (let i = 0; i < COUNT; i++) {
const vNode = {
type: 'div'
}
vNodes.push(vNode)
}
console.timeEnd('js 耗时')
</script>
</body>
在以上代码中,我们首先标记了一个数量值 COUNT
它表示循环执行的次数。然后我们利用 console.time
和 console.timeEnd
来记录执行时间,这里打印出来的时间就是我们的耗时。
此时,第一次的打印为 真实DOM 的耗时,第二次的打印为 JS 数组操作的耗时
最终打印结果如下:
通过耗时对比我们可以很清楚的看到:在简单的 DOM 操作与 简单的数据操作 对比下,DOM 操作的耗时大约是数据操作的 23 倍左右。
3、DOM 操作与 VNode 操作对比
<body>
<div id="app"></div>
<script>
const COUNT = 10000
console.time('dom 耗时')
for (let i = 0; i < COUNT; i++) {
const ele = document.createElement('div')
ele.innerHTML = 'hello bunian'
document.querySelector('#app').appendChild(ele)
}
console.timeEnd('dom 耗时')
const { h } = Vue
console.time('js 耗时')
const vNodes = []
for (let i = 0; i < COUNT; i++) {
const vNode = h('div', 'hello bunian')
vNodes.push(vNode)
}
console.timeEnd('js 耗时')
</script>
</body>
在这个代码中,我们使用了 Vue 中的 h 函数来构建了 Vnode,这样构建出的 VNode 对象结构上会更加复杂。从而可以验证出 当对象的结构变得更加复杂时,对 JS 耗时的影响会有多大
执行以上代码,得出的结果如下:
根据执行结果我们可以看出:当使用到了 h 函数时,JS 耗时明显增加。增加耗时的原因有两个:
- h 函数生成对象的耗时
- vNode 变得更加复杂时,push 的耗时
那么,假如 VNode 变得超级复杂的时候,JS 的耗时会超过 真实DOM 操作吗?
我们来看下一次的实验
4、DOM 操作与 复杂VNode 操作对比
<body>
<div id="app"></div>
<div id="app2"></div>
<script>
const COUNT = 10000
console.time('dom 耗时')
for (let i = 0; i < COUNT; i++) {
const ele = document.createElement('div')
ele.innerHTML = 'hello bunian'
document.querySelector('#app').appendChild(ele)
}
console.timeEnd('dom 耗时')
const { h, render } = Vue
console.time('js 耗时')
const vNodes = []
for (let i = 0; i < COUNT; i++) {
const vNode = h('div', { id: 'container' }, [
h('h1', null, ['Hello, World!']),
h('ul', null, [
h('li', null, ['Item 1']),
h('li', null, ['Item 2']),
h('li', null, ['Item 3'])
])
]);
// vNodes.push(vNode)
render(document.querySelector('#app2'), vNode)
}
console.timeEnd('js 耗时')
</script>
</body>
在这个代码中,我们使用 h 函数构建了一个 嵌套 的 VNode 结构,这个结构其实就比较复杂了。咱们来看看它的执行结果:
在这个执行结果中,我们明确的发现 虚拟 的耗时已经是 真实 DOM 耗时的 40倍 差距了。
看到这里可能有些同学会说:这是因为虚拟DOM 远比 真实 DOM 复杂的原因导致的。所以接下来咱们就把两边的复杂性调整一直,然后看看最终的测试数据
5、复杂DOM 操作与 复杂VNode 操作对比
<body>
<div id="app"></div>
<div id="app2"></div>
<script>
const COUNT = 10000
console.time('dom 耗时')
for (let i = 0; i < COUNT; i++) {
// 创建 div 元素
const container = document.createElement('div');
container.id = 'container';
// 创建 h1 元素
const h1 = document.createElement('h1');
h1.textContent = 'Hello, World!';
// 创建 ul 元素
const ul = document.createElement('ul');
// 创建三个 li 元素,并添加到 ul 元素中
const li1 = document.createElement('li');
li1.textContent = 'Item 1';
ul.appendChild(li1);
const li2 = document.createElement('li');
li2.textContent = 'Item 2';
ul.appendChild(li2);
const li3 = document.createElement('li');
li3.textContent = 'Item 3';
ul.appendChild(li3);
// 将 h1 和 ul 元素添加到 container 元素中
container.appendChild(h1);
container.appendChild(ul);
// 将 container 元素添加到 #app2 元素中
const app = document.querySelector('#app');
app.appendChild(container);
}
console.timeEnd('dom 耗时')
const { h, render } = Vue
console.time('js 耗时')
const vNodes = []
for (let i = 0; i < COUNT; i++) {
const vNode = h('div', { id: 'container' }, [
h('h1', null, ['Hello, World!']),
h('ul', null, [
h('li', null, ['Item 1']),
h('li', null, ['Item 2']),
h('li', null, ['Item 3'])
])
]);
// vNodes.push(vNode)
render(document.querySelector('#app2'), vNode)
}
console.timeEnd('js 耗时')
</script>
</body>
以上代码维持了两边同样的复杂程度,大家可以猜一猜最终的性能消耗会是什么情况呢?
查看最终的打印结果可以发现:
在双方都足够复杂的情况下,虚拟DOM 的性能消耗是远超 真实DOM 的。
但是,大家一定要注意:
以上测试结果只是在 初始渲染 的情况下的测试数据。
因为在实际开发中,DOM 的渲染是非常复杂的,涉及到:渲染、更新、删除 等各种复杂的操作,所以说我们 不能够 仅通过这一次的测试数据得出所有场景下的结论。
以上测试数据仅能够表示:初始渲染。
总结
框架一定是在基础 API 之上的封装。 如果我们把基础 API 的性能消耗比作为 1,那么框架的性能消耗一定是 1+N 的。这也就意味着 执行同样的操作时,框架的性能消耗一定 > 基础 API 的性能消耗。
所以说,虚拟DOM渲染所消耗的性能 > 真实DOM渲染所消耗的性能 就不足为奇了。
那么为什么还要使用到虚拟 DOM 呢?其实是因为当 DOM 更新时 ,虚拟DOM 可以帮助我们进行计算,从而减少 DOM 更新的频率,从而提升性能。