什么是VirtualDOM
VirtualDOM是对DOM的抽象,本质上是JavaScript对象,这个对象就是更加轻量级的对DOM的描述.
为什么需要VirtualDOM
既然我们已经有了DOM,为什么还需要额外加一层抽象?
首先,我们都知道在前端性能优化的一个秘诀就是尽可能少地操作DOM,不仅仅是DOM相对较慢,更因为频繁变动DOM会造成浏览器的回流或者重回,这些都是性能的杀手,因此我们需要这一层抽象,在patch过程中尽可能地一次性将差异更新到DOM中,这样保证了DOM不会出现性能很差的情况.
其次,现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率.
最后,也是VirtualDOM最初的目的,就是更好的跨平台,比如Node.js就没有DOM,如果想实现SSR(服务端渲染),那么一个方式就是借助VirtualDOM,因为VirtualDOM本身是JavaScript对象.
VirtualDOM的关键要素
VirtualDOM的创建
我们已经知道VirtualDOM是对真实DOM的抽象,根据不同的需求我们可以做出不同的抽象,比如
snabbdom.js的抽象方式是这样的.
当然,snabbdom.js由于是面向生产环境的库,所以做了大量的抽象各种,我们由于仅仅作为教程理解,因此采用最简单的抽象方法:
在明确了我们抽象的VirtualDOM构造之后,我们就需要一个函数来创建VirtualDOM.
这个函数很简单,接受一定的参数,再根据这些参数返回一个对象,这个对象就是DOM的抽象.
VirtualDOMTree的创建
上面我们已经声明了一个vnode函数用于单个VirtualDOM的创建工作,但是我们都知道DOM其实是一个Tree,我们接下来要做的就是声明一个函数用于创建DOMTree的抽象--VirtualDOMTree.
VirtualDOM的更新
VirtualDOM归根到底是JavaScript对象,我们得想办法将VirtualDOM与真实的DOM对应起来,也就是说,需要我们声明一个函数,此函数可以将vnode转化为真实DOM.
上述函数其实工作很简单,就是根据type生成对应的DOM,把data里定义的各种属性设置到DOM上.
VirtualDOM的diff
VirtualDOM的diff才是整个VirtualDOM中最难理解也最核心的部分,diff的目的就是比较新旧VirtualDOMTree找出差异并更新.
可见diff是直接影响VirtualDOM性能的关键部分.
要比较VirtualDOMTree的差异,理论上的时间复杂度高达O(n^3),这是一个奇高无比的时间复杂度,很显然选择这种低效的算法是无法满足我们对程序性能的基本要求的.
好在我们实际开发中,很少会出现跨层级的DOM变更,通常情况下的DOM变更是同级的,因此在现代的各种VirtualDOM库都是只比较同级差异,在这种情况下我们的时间复杂度是O(n).
那么我们接下来需要实现一个函数,进行具体的diff运算,函数updateChildren的核心算法如下:
我们可以假设有旧的Vnode数组和新的Vnode数组这两个数组,而且有四个变量充当指针分别指到两个数组的头尾.
重复下面的对比过程,直到两个数组中任一数组的头指针超过尾指针,循环结束:
头头对比:对比两个数组的头部,如果找到,把新节点patch到旧节点,头指针后移尾尾对比:对比两个数组的尾部,如果找到,把新节点patch到旧节点,尾指针前移旧尾新头对比:交叉对比,旧尾新头,如果找到,把新节点patch到旧节点,旧尾指针前移,新头指针后移旧头新尾对比:交叉对比,旧头新尾,如果找到,把新节点patch到旧节点,新尾指针前移,旧头指针后移利用key对比:用新指针对应节点的key去旧数组寻找对应的节点,这里分三种情况,当没有对应的key,那么创建新的节点,如果有key并且是相同的节点,把新节点patch到旧节点,如果有key但是不是相同的节点,则创建新节点我们假设有新旧两个数组:
旧数组:[1,2,3,4,5]新数组:[1,4,6,,,5]
首先我们进行头头对比,新旧数组的头部都是1,因此将双方的头部指针后移.
我们继续头头对比,但是2!==4导致对比失败,我进入尾尾对比,5===5,那么尾部指针则可前移.
现在进入新的循环,头头对比2!==4,尾尾对比4!==,此时进入交叉对比,先进行旧尾新头对比,即4===4,旧尾前移且新头后移.
接着再进入一个轮新的循环,头头对比2!==6,尾尾对比3!==,交叉对比2!=3!=6,四种对比方式全部不符合,如果这个时候需要通过key去对比,然后将新头指针后移
继续重复上述对比的循环方式直至任一数组的头指针超过尾指针,循环结束.
在上述循环结束后,两个数组中可能存在未遍历完的情况:循环结束后
先对比旧数组的头尾指针,如果旧数组遍历完了(可能新数组没遍历完,有漏添加的问题),添加新数组中漏掉的节点
再对比新数组的头尾指针,如果新数组遍历完了(可能旧数组没遍历完,有漏删除的问题),删除旧数组中漏掉的节点