首先介绍一下snabbdom,snabbdom是著名的虚拟DOM库,是diff算法的鼻祖,Vue源码借鉴了snabbdom。
1、什么是虚拟DOM
虚拟DOM是用JavaScript对象描述DOM的层次结构。DOM中的一切属性都在虚拟DOM中有对应的属性。本质上是JS 和 DOM 之间的一个映射缓存。虚拟DOM就是为了提高页面渲染性能。
要点:虚拟 DOM 是 JS 对象;虚拟 DOM 是对真实 DOM 的描述。
为什么要使用虚拟DOM?
用JS对象模拟DOM节点的好处是,页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制。
diff发生在虚拟DOM上。diff算法是在新虚拟DOM和老虚拟DOM进行diff(精细化比对),实现最小量更新,最后反映到真正的DOM上。
1)最小量更新。key是vnode节点的唯一标识,告诉了diff算法,更改前后他们是同一个节点。
2)只有是同一个虚拟节点,才进行精细化比较,否则就暴力删除旧的、插入新的。
3)只进行同层比较,不会进行跨层比较。
2、h函数
我们前面知道diff算法发生在虚拟DOM上,而虚拟DOM是如何实现的呢?实际上虚拟DOM是有一个个虚拟节点组成。
h函数用来产生虚拟节点(vnode)。虚拟节点有如下的属性:
1)sel: 标签类型,例如 p、div;
2)data: 标签上的数据,例如 style、class、data-*;
3)children :子节点;
4) text: 文本内容;
5)elm:虚拟节点绑定的真实 DOM 节点;
通过h函数的嵌套,从而得到虚拟DOM树。
我们编写了一个低配版的h函数,必须传入3个参数,重载较弱。
* 形态1:h('div', {}, '文字')
* 形态2:h('div', {}, [])
* 形态3:h('div', {}, h())
首先定义vnode节点,实际上就是把传入的参数合成对象返回。
然后编写h函数,根据第三个参数的不同进行不同的响应。
3、patch函数
如何定义是同一个节点呢?旧节点的key要和新节点的key相同且选择器也相同。
当调用patch函数时,会传入旧、新节点。首先我们判断旧节点oldVnode是否是虚拟节点,如果传入的不是虚拟节点是DOM节点,我们需要进行包装成虚拟节点,即调用vnode函数进行转化。
然后判断oldVnode和newVnode是不是同一个节点,如果是则进行精细化比较(这个后面再完成);若不是,则将新节点创建为DOM,然后添加到页面中,并移除旧节点。
createElement函数用来创建节点,将vnode节点创建为DOM。若vnode节点中存在嵌套,我们需要递归调用createElement完成子节点的创建。若为文本或undefined,则为直接转换为DOM。
如果oldVnode和newVnode是同一个节点,我们需要继续进行判断比较。首先判断oldVnode和newVnode是同一个对象,是则什么都不做,若不是,
判断newVnode有没有text属性,有则判断text相不相同,不同则用新的text属性代替;
若newVnode没有text属性,意味着有newVnode有children,再判断oldVnode有没有children,没有(意味着有oldVnode有text),清空oldVnode的text,将newVnode的children添加到DOM中;
若oldVnode有children,则需要进行diff了(后面再续)。
4、diff算法
当我们进行比较的过程中,我们采用的4种命中查找策略:
1)新前与旧前:命中则指针同时往后移动。
2)新后与旧后:命中则指针同时往前移动。
3)新后与旧前:命中则涉及节点移动,那么新后指向的节点,移到旧后之后。
4)新前与旧后:命中则涉及节点移动,那么新前指向的节点,移到旧前之前。
命中上述4种一种就不在命中判断了,如果没有命中,就需要循环来寻找,移动到旧前之前。直到while(新前<=新后&&旧前<=就后)不成立则完成。
如果是新节点先循环完毕,如果老节点中还有剩余节点(旧前和旧后指针中间的节点),说明他们是要被删除的节点。
如果是旧节点先循环完毕,说明新节点中有要插入的节点。
当新老VNode节点的start相同时,直接patchVnode,同时新老VNode节点的开始索引都加 1
当新老VNode节点的end相同时,同样直接patchVnode,同时新老VNode节点的结束索引都减 1
当老VNode节点的start和新VNode节点的end相同时,这时候在patchVnode后,还需要将当前真实dom节点移动到oldEndVnode的后面,同时老VNode节点开始索引加 1,新VNode节点的结束索引减 1
当老VNode节点的end和新VNode节点的start相同时,这时候在patchVnode后,还需要将当前真实dom节点移动到oldStartVnode的前面,同时老VNode节点结束索引减 1,新VNode节点的开始索引加 1
如果都不满足以上四种情形,那说明没有相同的节点可以复用,则会分为以下两种情况:
从旧的VNode为key值,对应index序列为value值的哈希表中找到与newStartVnode一致key的旧的VNode节点,再进行patchVnode,同时将这个真实dom移动到oldStartVnode对应的真实dom的前面
调用createElm创建一个新的dom节点放到当前newStartIdx的位置。