Virtual DOM在前端开发中有广泛的应用场景,React/Vue等前端框架的渲染都是基于Virtual DOM的,当然原理都大同小异,本文详细分析下这种机制的实现。

Vue的virtual DOM机制是基于snabbdom的,那我们就从这个框架入手。

github地址:https://github.com/snabbdom/snabbdom

搭建调试环境

先用webpack搭建一个调试环境,方便我们写一些代码测试。

npm引入下snabbdom和webpack,然后写个webpack配置(webpack.config.js)

编写webpack.config.js

VNode的定义

vnode.ts中定义了vnode的数据结构,每一个vnode表示virtual dom中的一个节点。
virtual dom这种机制相当于把dom操作进行解耦,开发者只需关心virtual dom相关的api,而不需要进行具体的dom操作,

其中sel表示当前vnode代表的标签,比如当前vnode是一个div标签, 那么sel就是div。

data是描述的是vnode中的数据,包括style,class,listener等。使用vnode描述dom元素的话,那么任何可能出现在标签上的数据都可以看成是VNodeData。
定义如下:

children字段,表示当前vnode的所有子节点,同时它们每个子节点也是VNode类型。

key字段,用数据标识当期的vnode,key在patch的过程中非常有用,如果vnode定义了key,那么可以快速判断出oldVnode中是否存在该节点,以便进行复用,减少重新创建Element的开销。

创建VNode

使用h进行创建vnode,我们看下实现:

上面定义了一些h的重载函数,我们只需关注一个逻辑就好,第一个参数时sel标识当前vnode是哪个标签,第二个参数可以传入VNodeChildren或者VNodeData均可。
第三个参数若有则必须传入VNodeChildren,同时第二个参数将被当成VNodeData进行解析。

最后看下创建VNode的函数:

举个例子,比如需要创建一个div标签,它的文字是Hello VirtualDOM,并且有一定的style,那么编写的代码如下:

第三个参数Hello VirtualDOM会变成vnode的text属性。

patch 过程

vnode只是内存中的数据,如何反映到DOM上呢?我们把这个根据vnode更新dom的过程称为patch。

通过patch,可以比较出两个vnode之间的不同,然后把新的vnode数据更新到DOM上。这个过程其实最重要的就是比较,通过比较,尽可能的减少DOM操作,从而达到优化性能的目的。

官方demo给了我们一个通过vnode更新DOM的例子:

先看下patch函数的定义:

我们通过一张流程图来看下patch函数的流程:

patch流程

其中首先会判断是否是同一个节点,这个过程由sameNode实现,这么做的原因也不难理解,如果两个不同的标签,肯定是没有办法进行比较的,只能进行替换。

snameNode的实现:

如果创建vnode的时候没有指定key,那么key就是undifined。主要还是比较sel字段。

先看下简单的逻辑,如果不是同一种vnode,那么就需要重新创建一个element插入到dom,并且删除之前的。
代码如下:

createElm实现

上面的patch函数中说道,vnode是虚拟dom,它是通过createElm函数将vnode变成 html的Element类型的,具体见下面代码。

可以看出这段逻辑还是比较简单的,重点在于同一种类节点的比较,也就是patchVnode的过程。

patchVnode实现

patchVnode可以看成是patch函数的内部流程,具体一点说就是进行两个vnode的比较,而patch函数可以进行vnode和Element之间的比较。

我们先通过一张图看下patchVnode的工作流程:

patchVNode流程

patchVnode实现如下:

从代码中可以看到,最复杂的还是两个vnode都有子节点的情况(如流程图中红色部分),这个情况会进入一个新函数updataChildren进行处理。updateChildren会对子节点进行递归的patchVnode,直到根子节点为止。

其他两种场景较为简单,无非dom上增加和删除多出来的子节点。

下面我们就重点看下uodateChildren的实现

updateChildren实现

updateChildren的作用是比较两个vnode之间的children部分,并将结果更新到dom上,在patchVnode函数中被调用,但是在函数内部也会调动patchVnode比较子节点,是一个递归的过程。

updateChildren的流程比较复杂,我画了个流程图帮助理解。

核心逻辑是分别从头尾两个位置来比较这两个children数组,如果使用sameNode判断可以进行比较的话,那么会将这个子节点进入下一个patchVnode流程,同时移动头尾位置。

根据key进行查找

流程图的红色部分是一处比较关键的逻辑,如果新旧vnode的children头尾比较都失败了,那么就会根据vnode的key进行查找,关键代码如下。

其中createKeyToOldIdx是创建一个旧vnode的map。

待补充

发表评论

邮箱地址不会被公开。 必填项已用*标注