Vue
通过虚拟DOM
实现对页面渲染的监听更新,降低对真实DOM
的操作。而patch
则是Vue
虚拟DOM
实现的基石,它能快速实现虚拟DOM
的对比更新,并最终将vnode
渲染成真实DOM
。整个patch
的过程就是:创建节点、删除节点和修改节点的过程。
创建节点
当因状态改变而新增的节点在DOM
中并不存在时,我们需要创建一个节点并插入到DOM
中。即当oldVnode
不存在而vnode
存在时,就需要使用vnode
生成真实的DOM
元素并将其插入到视图当中。
1、首次渲染时,DOM
中不存在任何节点,即oldVnode
是不存在的,我们需要使用vnode
创建一个新DOM
节点并渲染视图。
2、当vnode
和oldVnode
完全不是同一个节点时,即oldVnode
是一个被废弃的节点,vnode
是一个全新的节点,此时,我们需要使用vnode
创建一个新DOM
节点,用它去替换oldVnode
所对应的真实DOM
节点。
因vnode
是有类型的,所以当我们在创建节点时,最重要的是根据vnode
的类型来创建出相同类型的DOM
元素。事实上,只有三种类型的节点会被创建并插入到DOM
中:元素节点
、注释节点
和文本节点
。
删除节点
因为渲染视图时,需以vnode
为标准,所以vnode
中不存在的节点都属于被废弃的节点,需要从DOM
中删除。当vnode
和oldVnode
完全不是同一个节点时,在DOM
中需要使用vnode
创建新节点替换oldVnode
所对应的旧节点,而替换的过程就是将新创建的DOM节点
插入到旧节点的旁边,然后再将旧节点删除。
在要删除节点的父元素上调用removeChild
方法即可。
更新节点
无论是新增节点,还是删除节点,他们之间都有一个共同点,那就是两个虚拟节点是完全不同的。而当新旧两个节点是相同的节点时,我们需要对这两个节点进行比较细致的比对,然后对oldVnode在视图中所对应的真实节点进行更新。
更新节点需要对以下3种情况进行判断并分别处理:
- 如果
VNode
和oldVNode
均为静态节点:无需处理 - 如果
VNode
是文本节点:如果VNode
是文本节点即表示这个节点内只包含纯文本,那么只需看oldVNode
是否也是文本节点,如果是,那就比较两个文本是否不同,如果不同则把oldVNode
里的文本改成跟VNode
的文本一样。如果oldVNode
不是文本节点,那么不论它是什么,直接调用setTextNode
方法把它改成文本节点,并且文本内容跟VNode
相同。 - 如果VNode是元素节点:如果VNode是元素节点,则又细分两种情况
- 该节点不包含子节点
- 该节点包含子节点
patch流程
1、当oldVnode
不存在时,直接使用vnode
渲染视图;
2、当oldVnode
和vnode
都存在但并不是同一个节点时,使用vnode
创建的DOM
元素替换旧的DOM
元素;
3、当oldVnode
和vnode
是同一个节点时,使用更详细的对比操作对真实的DOM
节点进行更新。
patch
过程中最关键的是运营Vue-Diff
算法,完成新旧vnode
的精细化对比。通过Diff
算法,我们可以计算出虚拟DOM
中被改变的部分,然后针对该部分进行原生DOM
操作,而不用重新渲染整个页面,从而提升性能。
原始diff算法
原始diff算法就是,两个虚拟DOM树,进行不分层级的逐一比对,也就是说,一个虚拟DOM树,从根节点到以后分支的每一个节点,都要单独拿出来跟新生成的节点做比较,这就是最原始的diff算法。
这个diff算法的时间复杂度表面上看是(n ^2),因为单独一个个的去跟另外的n个相比较,肯定是n ^2次就比较结束了,但是实际上不是的,比较完之后还要计算如何在最优的地方放置最佳的节点,所以就是O(n ^3)了。
虽然原始的Diff算法从功能上解决了先对比再处理实际DOM的需求,但是实际上我们的流程变得更加的复杂和笨拙。
优化Diff算法
优化Diff
算法只比较同一层级 ,不做跨级比较。因为在实际的web
展示中,非同级的节点移动是非常少的,所以可以选择做同级比较。
所谓同级比较,即只比较同层的节点,不同层不做比较。不同层的只需要删除原节点,并且新建插入更新节点。
Vue-Diff
算法,采用的就是优化过的Diff算法,同层比较,不会跨级,且其比较是从两侧向中间进行的,这种方式相对于从左到右依次比对的方式来说,更高效。
Vue-diff 策略
1、Tree Diff
Tree Diff是对树每一层进行遍历,找出不同。
2、Component Diff
Vue是基于组件构建的,对于组件间的比较采用的策略如下:
- 如果是同一类型的组件,则按照原策略比较组件的虚拟 DOM 树,否则不需要比较。
- 如果是不同类型的组件,则将该组件判断为dirty component,从而替换整个组件下的所有子节点。
3、Element Diff
在进行组件对比的时候,如果两个组件类型相同,则需要进行元素级别的对比,这就是Element Diff
。Element Diff
时,提供了3种节点操作,分别为INSERT_MARKUP
(插入),MOVE_EXISTING
(移动),REMOVE_NODE
(删除)。
INSERT_MARKUP
:新的组件类型不在旧集合中,即全新的节点,需要对新节点进行插入操作。MOVE_EXISTING
:旧集合中有新组件类型,且element
是可更新的类型,这时候就需要做移动操作,可以复用以前的DOM
节点。REMOVE_NODE
:旧组件类型,在新集合里也有,但对应的element
不同则不能直接复用和更新,需要执行删除操作,或者旧组件不在新集合里的,也需要执行删除操作。
Vue-diff过程
定义4个指针:OldStartIdx、OldEndIdx、NewStartIdx、NewEndIdx
。比较的是两个指针对应的节点的虚拟DOM是否为同一个。
具体步骤如下:
- 比较
OldStartIdx
和NewStartIdx
- 如果两个
startIdx
相同,则两个指针都会+1
,也就是向后移一位,重新生成OldStartIdx
和NewStartIdx
指针。 - 如果两个
startIdx
不一致,则比较两个endIdx
- 比较
OldEndIdx
和NewEndIdx
- 如果两个
endIdx
一致,则两个endIdx
都减1,也就是向前移一位,再执行步骤1。 - 如果两个
startIdx
和两个endIdx
都不一致,则比较捺向的oldStartIdx
和NewEndIdx
- 如果
oldStartIdx
和NewEndIdx
一致,则把oldStartIdx
指向的虚拟DOM
里的真实DOM
节点,挪到OldEndIdx
位置之后,oldStartIdx
加1向后移一位,newEndIdx
减1向前移动一位。 - 如果竖向和捺向都不一致,则比较撇向
oldEndIdx
和NewStartIdx
。 - 如果撇向一致,则把
oldEndIdx
指向的真实dom
节点挪到oldStartIdx
所在的真实dom
前,同时oldEndIdx
减1向前移动一位,newStartIdx
加1向后移动一位。 - 如果竖向、捺向、撇向都不一致,则看有没有key。
- 如果有
key
,就能快速找到,并挪到oldStartIdx
前。 - 如果没有
key
,就遍历oldStartIdx
和oldEndIdx
之间的所有节点,寻找newStartIdx
指向的节点是否存在于这些老的vdom
中。如果有,就把它挪到oldStartIdx
前;没有就在oldStartIdx
之前创建一个节点,newEndIdx
减1向前移动一位。这样比较下去,一直到newEndIdx<newStartIdx
。
当newEndIdx<newStartIdx
,这时new vnode
生成完毕,然后将old vnode
中多余的部分删掉即可,也就是oldStartIdx
和oldEndIdx
指向的dom
及中间的部分。