什么是虚拟dom
当说起vue和react时候,大家都不免会提到一个概念,就是Virtual DOM(虚拟Dom)。那么,这个虚拟Dom到底是个什么东西,为什么这两个伟大的框架都要使用呢。
首先Virtual DOM是一个映射真实DOM的JavaScript对象,如果需要改变任何元素的状态,那么是先在Virtual DOM上进行改变,而不是直接改变真实的DOM。当有变化产生时,一个新的Virtual DOM对象会被创建并计算新旧Virtual DOM之间的差别。之后这些差别会应用在真实的DOM上。
首先我们举个例子:
<ul class="list">
<li>item 1</li>
<li>item 2</li>
</ul>
对于上面的这个Dom树,我们可以简单的用javascript进行描述。
{
type: 'ul',
props: {'class': 'list'},
children: [
{ type: 'li', props: {}, children: ['item 1'] },
{ type: 'li', props: {}, children: ['item 2'] }
]
}
当然,上面的这个Dom树是比较简单的Dom,真实的Dom树远比这个要复杂的多。但是其本质上都是一样的,就是一个嵌套着数组的原生对象。
当新一项被加进去这个JavaScript对象时,一个函数会计算新旧Virtual DOM之间的差异并反应在真实的DOM上。计算差异的算法是高性能框架的秘密所在,React和Vue在实现上有点不同。
Vue宣称可以更快地计算出Virtual DOM的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
而对于React而言,每当应用的状态被改变时,全部子组件都会重新渲染。当然,这可以通过shouldComponentUpdate这个生命周期方法来进行控制,但Vue将此视为默认的优化。
为什么要用虚拟Dom
这个问题,其实就是为了对比虚拟Dom和真实Dom的区别,以及使用虚拟Dom有什么优势。很多人都觉得虚拟Dom比真实Dom快。但是真是如此么?
首先我们举个例子:向页面中插入一个元素,不管是按钮还是别的,我们会怎么去做:
document.getElementById('box').innerHTML = '<button>按钮</button>'
但是如果我们向一个盒子中添加10000个button呢,这个时候我们会怎么去做;
var _d1 = new Date();
for(var index = 0; index < 10000; index ++) {
var button = document.createElement('button');
button.innerText = '按钮'
document.getElementById('box').append(button);
}
var _d2 = new Date();
console.log(_d2 - _d1)
我们可以一个一个去网页里添加,但是当我们执行这段代码的时候,我们会发现,添加的耗时已经,打印出来了,但是浏览器,还要一会儿才会输出内容。其实这个过程是在进行重排重绘,对于有些浏览器来说,会在我们添加完每一个具有占位的dom的时候,都会进行重排和重绘,我么知道,重排和重绘其实代价是非常的昂贵的。因此我们需要对上述的代码进行优化,优化的结果如下:
var _d1 = new Date();
var _dom = ''
for(var index = 0; index < 10000; index ++) {
_dom += '<button>按钮</button>'
}
document.getElementById('box').innerHTML = _dom;
var _d2 = new Date();
console.log(_d2 - _d1)
我么对比一下上面的两组结果第一个是40ms,第二个是16ms,经过这么对比,我们明显能够发现,其实后者的效率要高于前者。
如果我们创建了这10000个按钮,我们希望对其中的某些按钮进行修改,一种方案就是我们将所需要进行修改的按钮,进行重新生成,组织成dom,然后通过innerHTML插入到页面中,进行重排和重绘;另外一种方案就是我们可以通过找到对应的按钮进行修改,然后在将修改结果逐个设置上去,然后发生重排和重绘,这样的性能的消耗其实也是很严重的。
var _d1 = new Date();
var buttons = document.getElementById('box').childNodes
buttons[100].style.backgroundColor = 'red'
buttons[100].style.width = '500px'
var _d2 = new Date();
console.log(_d2 - _d1)
针对上面的种写法其实对于浏览器来说是很快的。但是既然用实体Dom很快,为什么还要用虚拟Dom呢,首先我们还是从重排和重绘的角度说起,针对于上面的这个操作,我们更新视图,其实是发生了一次重排,和两次重绘。一次重排,就是我们设置了一个按钮的宽度buttons[100].style.width = '500px'
,两次重绘第一次,是我们设置按钮的背景色,第二次其实是我们设置了按钮的宽度引起了重排,重排引起了重绘造成的。这只是一个简单的视图更新。但是如果对于一个复杂的视图进行更新的话,可能发生的重排和重绘的次数更多。但是我们并不关心它到底发生了多少次重排和重绘,我们只关心最后的结果。但是中间的这些重排重绘的结果我们是不是能避免就避免了,毕竟重排和重绘的代价是很大的。
如何减少重排和重绘呢有以下几点方法:
- 隐藏元素,应用修改,重新显示
- 使用文档片段,在当前Dom之外构建一个子树,再把拷贝回文档
- 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素
- 使用虚拟Dom
其虚拟Dom的本质就是前面2,3的一个方案。那么也就是说虚拟DOM之所以能够提高性能,不是说不操作DOM,也不是是说虚拟Dom查找Dom比实际的Dom快(其实大家都知道JS的执行效率是不如C++的,所以说同样的算法的,js的查找Dom的速度其实是不容C++的,也就是说不如实体Dom的),而是减少操作DOM的次数,减少回流和重绘的次数。
换句话说就是虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能
- 用 JavaScript 对象结构表示 DOM 树的结构;
- 然后用这个树构建一个真正的 DOM 树,插到文档当中;
- 当状态变更的时候,重新构造一棵新的对象树;
- 然后用新的树和旧的树进行比较,记录两棵树差异;
- 把 2所记录的差异应用到步骤 2)所构建的真正的 DOM 树上,视图就更新了
使用diff算法比较新旧虚拟DOM----即比较两个js对象不怎么耗性能,而比较两个真实的DOM比较耗性能,从而虚拟DOM极大的提升了性能。
接下来我们继续从操作的角度来聊虚拟Dom和实体Dom
- nnerHTML: render html string O(template size) + 重新创建所有 DOM 元素 O(DOM size)
- Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change)
Virtual DOM render + diff 显然比渲染 html 字符串要慢(渲染Html字符串不会进行对比两个真实的Dom,因此少了Diff的这一步。)。但是!它依然是纯 js 层面的计算,比起后面频繁的通过js去操作 DOM来说,依然便宜了太多(js操作Dom,首先要解析js,然后调用浏览的api,但后获取结果,返回给js这个过程其实也是有代价的)。可以看到,innerHTML 的总计算量不管是 js 计算还是 DOM 操作都是和整个界面的大小相关,但 Virtual DOM 的计算量里面,只有 js 计算和界面大小相关,DOM 操作是和数据的变动量相关的。前面说了,和 DOM 操作比起来,js 计算是极其便宜的。这才是为什么要有 Virtual DOM:它保证了
- 不管你的数据变化多少,每次重绘的性能都可以接受
- 你依然可以用类似 innerHTML 的思路去写你的应用。
但是针对一个特别大的项目,而且交互复杂的项目来说,我们面临着两个问题,一个是可维护性,一个是代码的执行性能。针对于代码执行的性能来说,我们可以通过纯手工的优化 DOM 操作,针对于每一个组件我们都可以进行优化,并且构建一个应用。但是那样又有什么用,最后搞得整个项目一团糟,最后完全失去了可维护性,针对于维护性我们可以通过使用框架的虚拟Dom进行提高项目的可维护性,但是因为我们的虚拟的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的,所以会丢失部分性能。因此在超高性能和可维护性方面,我们选择了比较折中的办法就是使用虚拟dom,然后生成框架。
所以说为什么会使用虚拟Dom
- 减少不必要的重排和重绘(提高性能)
- 提高项目的可维护性(会降低部分性能,但是对于可维护性的角度来说,这部分性能的损耗是能够接受的)
小结
如果你的应用中,交互复杂,需要处理大量的UI变化,那么使用Virtual DOM是一个好主意。如果你更新元素并不频繁,那么Virtual DOM并不一定适用,性能很可能还不如直接操控DOM。