虚拟DOM简介
什么是虚拟DOM?
我们现在使用的三大主流框架Vue.js、Angular和React都是声明式操作DOM。我们通过描述状态和DOM之间的映射关系是怎样的,就可以将状态渲染成试图。关于状态到视图的转化过程,框架会帮我们做,不需要我们自己去操作DOM。
状态可以是JavaScript中的任意类型。Object、Array、String、Number、Boolean等都可以作为状态,这些状态可能最终会以段落、表单、链接或按钮等元素呈现在用户界面上。
本质上,我们将状态作为输入,并生成DOM输出在页面上显示出来,这个过程叫做渲染
渲染的过程.PNG
然而通常在程序运行时,状态会不断发生改变(状态改变的原因有很多,可能是用户点击了某个按钮,可能是某个ajax请求,这些行为都是异步的)每当状态发生变化时,都需要重新渲染。如何确定状态中发生了什么变化以及需要在哪里更新DOM?
在这种情况下,最简单粗暴的方式是,不需要关心状态发生了什么变化,不需要关心哪里更新DOM,我们只要把所有DOM删除了,然后使用状态重新生成一份DOM,并将其输出到界面上。
但是访问DOM是非常昂贵的,按照上面的方式,会造成相当多的性能浪费。状态变化通常只是有限的几个节点需要重新渲染,所有我们不仅需要找出哪里需要更新,还需要尽可能少的访问DOM。
状态发生变化.PNG
如上图所示,当某个状态发生变化时,只更新与这个状态相关联的DOM节点。
这个问题有很多种解决方案,目前,各大主流框架都有自己一套解决方案,在Angular中就是脏检查的流程,React中使用虚拟DOM,vuejs1.0通过细粒度的绑定。因此,虚拟DOM本质上只是众多解决方案中的一种,可以用但并不一定必须用。
虚拟DOM的解决方式是通过状态生成一个虚拟节点树,然后使用虚拟节点树进行渲染。在渲染之前,会使用新生成的虚拟节点数和上一次生成的虚拟节点树进行对比,只渲染不同的部分。
虚拟节点数其实是由组件树建立起来的整个虚拟节点(Virtual Node,也简写为Vnode)树。
虚拟节点树.PNG
为什么要引入虚拟DOM
事实上,Angular和React的变化侦测有一个共同点,那就是他们都不知道哪些状态变了。因此,就需要进行比较暴力的对比,React是通过虚拟DOM的比对,Angular是使用脏检查的流程。
Vue.js的变化侦测不一样,它在一定程度上知道具体哪些状态发生了变化,这样就可以通过更细粒度的绑定来更新视图。也就是说,在Vue.js中,当状态发生变化时,它在一定程度上知道哪些节点使用了这个状态,从而对这些节点进行更新操作,不需要对比。事实上,在vue.js 1.0中就是这样实现的。
但是这样做也有一定的代价,因为粒度太细,每一个绑定都会有一个对应得watcher来观察状态的变化,这样就会有一定的内存开销和追踪依赖的开销。当状态被越多的节点使用时,开销就越大。大型项目来说,这个开销是非常大。
因此,Vue.js 2.0中选择了中等粒度的解决方案,那就是引入了虚拟DOM。组件级别是一个watcher实例,就是说即便一个组件内有10个节点使用了某个状态,但其实也只有一个watcher在观察这个状态的变化。所以这个状态发生变化时,只能通知到组件,然后组件内部通过虚拟DOM去进行比对和渲染。
Vue.js 中的虚拟DOM
在vue.js中,我们使用模板来描述状态和DOM之间的映射关系。Vue.js通过编译将模板转化为渲染函数render,执行渲染函数就可以得到一个虚拟节点树,使用这个虚拟节点树就可以渲染页面。
模板转化为视图.PNG
虚拟DOM的终极目标是将虚拟节点(vnode)渲染到视图上。但是如果直接使用虚拟节点覆盖旧节点的话,会造成很多不必要的DOM操作。
例如一个ul标签下有很多li标签,其中只有一个li变化,这种情况下如果直接用新的ul替换旧的ul,其实除了那个发生了变化的li节点之外,其他节点都不需要重新渲染。
由于DOM操作比较慢,所以这些DOM操作在性能上会有一定的浪费。避免这些不必要的DOM操作会提升很大的性能。
为了避免不必要的DOM操作,虚拟DOM在虚拟节点映射到视图的过程中,将虚拟节点和上一次渲染视图所使用的的旧虚拟节点(oldVnode)进行对比。找出真正需要更新的节点来进行DOM操作,可以避免不必要改动的DOM。
图中给出了虚拟DOM的整体运行流程,先将vnode和oldVnode做对比,然后在更新视图
虚拟DOM执行过程.PNG
可以看出虚拟DOM在Vue.js中所做的事情并没有那么复杂,他主要做了两件事
- 提供与真实DOM节点所对应得虚拟节点vnode
- 将虚拟节点vnode和旧虚拟节点oldvnode进行对比,然后更新视图。
vnode是JavaScript中一个很普通的对象,这个对象的属性上保存了生成DOM节点所需要的一些数据。
对比两个虚拟节点是虚拟DOM中最核心的算法(即patch),他可以判断出哪些节点发生了变化,从而只对发生了变化的节点进行操作。
总结
虚拟DOM是讲状态映射成试图的众多解决方案之一,它的运作原理是使用状态生成虚拟节点,然后使用虚拟节点渲染成视图。
之所以需要先使用状态生成虚拟节点,是因为如果直接用状态生成真实的DOM,会有一定程度上的性能浪费。而先创建虚拟节点再渲染视图,就可以将虚拟节点缓存,然后使用新创建的虚拟节点和上一次缓存的虚拟节点进行对比,然后根据对比结果更新需要更新的DOM节点,避免不必要的DOM操作。
由于Vue.js的变化侦测粒度更细,所以挡状态发生变化时,vue.js知道的信息更多,一定程度上知道哪些位置使用了窗台。因此,vue.js可以通过细粒度的绑定来更新视图,vue.js 1.0 就是这样实现的。
但是这么做也有一定的代价。因为粒度太细,就会有很多的watcher同时观察这些状态,会有一定的内存开销和依赖追踪依赖的开销,所以vue.js 2.0 采取了中等粒度的解决方案。状态侦测不再是某个具体节点,而是某个组件,组件内部通过虚拟DOM来渲染视图,这样可以大大的缩减依赖数量和watcher数量。
Vue.js中通过模板来描述状态和视图之间的映射关系,所以会将模板编译成渲染函数render,然后执行渲染函数生成虚拟节点vnode,最后使用虚拟节点更新视图。
虚拟DOM在vue.js中所做的事是将虚拟节点vnode和旧虚拟节点oldVnode进行对比,根据对比结果来进行DOM操作来更新视图。