虚拟DOM简介
1. 什么是虚拟DOM
命令式操作DOM
的问题:
状态越来越多,DOM操作越来越频繁,使用命令式操作DOM就会发现:我们的代码中有很大一部分代码是用来操作DOM,程序中的状态很难管理,代码中的逻辑也很混乱。
Vue.js
、React
、Angular
都是声明式操作DOM,通过描述状态和DOM之间的映射状态关系是怎样的,就可以将状态渲染成视图。
状态可以是JavaScript
中的任意类型,这些状态最终会以HTML
标签呈现在用户界面上,具体的说是呈现在页面上。
我们将状态作为输入,并生成DOM输出到页面上显示出来,这个过程叫做渲染。理论上所有异步行为都有可能引起状态变化,每当状态发生变化时,都需要重新渲染。
如何确定状态中发生了什么变化以及需要在哪里更新DOM?
访问DOM是非常昂贵的,如果我们不关心状态发生了什么变化,也不关心哪里更新DOM,只需要把所有的DOM全部删除,然后用状态重新生成一份DOM,会造成相当多的性能浪费。
状态变化通常只有有限的几个节点需要新渲染,所以需要找到哪里需要更新,还需要尽可能的少访问DOM。当某个状态发生变化时,只更新与这个状态相关联的DOM节点。
虚拟DOM的解决方式是通过状态生成一个虚拟节点树,然后使用虚拟节点树进行渲染。在渲染之前,使用新生成的虚拟节点树和上次生成的虚拟节点树进行对比,只渲染不同的部分。
虚拟节点树其实是由组件树建立起来的整个虚拟节点(Virtual Node
,简写为vnode
)树。
2. 为什么引入虚拟DOM
Angular
、React
和Vue.js
变化侦测的不同:
Angular
和Raect
它们不知道哪些状态变化了,React
通过虚拟DOM的对比,Angular
是使用脏检查的流程。Vue.js
在一定程度上知道具体哪些状态发生了变化,这样就可以通过更细粒度的绑定来更新视图。
粒度太细导致的问题:
粒度太细,每一个绑定都会绑定一个对应的watcher
来观察状态的变化,这样就会有一些内存开销和一些依赖追踪的开销。当状态被越多的节点使用时,开销就越大。
解决办法:
Vue.js 2.0
开始选择了一个中等粒度的解决方案,就是引入了虚拟DOM。
组件级别是一个watcher
实例,即使一个组件内有10个节点使用了某个状态,但其实也只有一个watcher
在观察这个状态的变化。所以当这个状态发生变化时,只能通知到组件,然后组件内部通过虚拟DOM去进行对比渲染。
3. Vue.js
中的虚拟DOM
在Vue.js
中,我们使用模板来描述状态与DOM的映射关系。Vue.js
通过编译将模板装换成渲染函数(render
),执行渲染函数就可以得到一个虚拟节点树,使用这个虚拟节点数就可以渲染页面。
虚拟DOM的终极目标是将虚拟节点(vnode
)渲染到视图上。但是如果直接使用虚拟节点覆盖旧节点的话,会有很多不必要的DOM操作。
为了避免不必要的DOM操作,虚拟DOM在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点(oldVnode
)做对比,找出真正需要更新的节点来进行DOM操作,从而避免操作其他无任何改动的DOM。
虚拟DOM在Vue.js
中所做的事:
- 提供与真实DOM节点所对应的虚拟节点vnode
- 将虚拟节点vnode和旧虚拟节点oldVnode进行对比,然后更新视图。
vnode
是JavaScript
中一个很普通的对象,这个对象的属性上保存了生成DOM节点所需要的一些数据。
对两个虚拟节点进行比对是虚拟DOM中最核心的算法(patch
),它可以判断出哪些节点发生了变化,从而只对发生了变化的节点进行更新操作。
4.总结
虚拟DOM是将状态映射成视图的众多解决方案中的一种,它的作用原理是使用状态生成虚拟节点,然后使用虚拟节点渲染视图。
之所以要先使用状态生成虚拟节点,是因为如果直接使用状态生成真实的DOM,会有一定程度的性能浪费。而先创建虚拟节点再渲染视图,就可以将虚拟节点缓存,然后使用新创建的虚拟节点和上一次渲染时缓存的虚拟节点进行对比,然后根据对比结果,只更新需要更新的真实DOM节点,从而避免不必要的DOM操作,节省一定的性能开销。
由于Vue.js
的变化侦测粒度更细,所以当状态发生变化时,Vue.js
知道的信息更多,一定程度上可以知道哪些位置使用了状态。因此,Vue.js
可以通过细粒度的绑定来更新视图,Vue.js 1.0
就是这样实现的。
但是这样做也有一定的代价。因为粒度太细,就会有很多的watcher
同时观察某些状态,会有一些内存开销以及一些依赖追踪的开销,所以Vue.js 2.0
采取了一个中等粒度的解决方案,状态侦测不再细化到某个具体节点,而是某个组件,组件内部通过虚拟DOM来渲染视图,这可以大大缩减依赖数量和watcher
数量。
Vue.js
中通过模板来描述状态与视图之间的映射关系,所以它会先将模板编译成渲染函数,然后执行渲染函数生成虚拟节点,最后使用虚拟节点更新视图。
因此,虚拟DOM在Vue.js
中所做的事是提供虚拟节点vnode
和对新旧两个vnode
进行对比,并根据比对结果进行DOM操作来更新视图。