什么是虚拟DOM和diff算法 在前端发展的初期,前端工程师们使用JQ这样的框架对DOM进行命令式的操作,这种方式最大的好处是易学,直白,但在前端页面和业务逻辑不断复杂的今天,命令式的操作dom已经不适合大型项目的开发,随之而来的三大框架,vue,react, angular均使用了声明式的操作DOM,开发者只要描述状态和和DOM的映射关系,框架就会帮我们把状态转化成渲染好的视图,虚拟DOM正是状态和真实DOM之间的一个中间层,为什么要有这个中间层呢,因为操作DOM的代价是非常大的,虚拟DOM可以描述描述真实dom的信息,通过对比前后的虚拟dom找出DOM里真正需要的部分,从而减少DOM操作,提高性能。比较前后虚拟dom的算法就是diff算法,这是整个虚拟dom机制的最核心优化之一。
VNode VNode本质上是一个JS对象,只是这个对象是用来描述真实DOM而已,Vue的实现如下。( 函数参数里的 ?: 的意思是,这个参数可以没有,但是如果有的话就要是某个指定的类型,这是flow的语法)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 export  default  class  VNode  {  constructor  (     tag?: string,     data?: VNodeData ,     children?: ?Array ,     text?: string,     elm?: Node ,     context?: Component ,     componentOptions?: VNodeComponentOptions ,     asyncFactory?: Function    ) {     this .tag  = tag      this .data  = data       this .children  = children      this .text  = text       this .elm  = elm       this .ns  = undefined       this .context  = context      this .fnContext  = undefined       this .fnOptions  = undefined       this .fnScopeId  = undefined       this .key  = data && data.key        this .componentOptions  = componentOptions      this .componentInstance  = undefined        this .parent  = undefined       this .raw  = false          this .isStatic  = false        this .isRootInsert  = true          this .isComment  = false        this .isCloned  = false        this .isOnce  = false          this .asyncFactory  = asyncFactory      this .asyncMeta  = undefined       this .isAsyncPlaceholder  = false     }      get child (): Component  | void  {     return  this .componentInstance    } } 
 
实现比较简单,我就不过多赘述了,这里顺带介绍三个VNode类型
文本节点,只有text属性
1 2 3 {     text : "hello world"  } 
 
注释节点,只有text属性和isComment属性
1 2 3 4 {     text : "hello world" ,     isComment : true  } 
 
元素节点,只有tag(标签名),data(节点数据),children(子节点列表),context(当前组件的vue.js实例)
1 2 3 4 5 6 {     tag : "div" ,     data : {...},     children : [{...},{...}],     context : {...} } 
 
这三种之外还有克隆节点,组件节点,函数式组件节点,不过本文讨论的重点不在这,所以暂且省略。
patch和patchVNode 将虚拟DOM渲染成真实DOM的操作是patch。patch中的核心是diff算法,diff算法做的一个很重要的优化就是,只比较同层次的节点,这使diff的时间复杂度降到了O(n)
  
  
patch在update方法中被调用,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 Vue .prototype  ._update  = function  (vnode: VNode, hydrating?: boolean ) {    const  vm : Component  = this      if  (vm._isMounted ) {       callHook (vm, 'beforeUpdate' )     }     const  prevEl = vm.$el      const  prevVnode = vm._vnode      const  prevActiveInstance = activeInstance     activeInstance = vm     vm._vnode  = vnode     if  (!prevVnode) {              vm.$el  = vm.__patch__ (         vm.$el , vnode, hydrating, false  ,         vm.$options ._parentElm ,         vm.$options ._refElm        )     } else  {                     vm.$el  = vm.__patch__ (prevVnode, vnode)     }     activeInstance = prevActiveInstance          if  (prevEl) {       prevEl.__vue__  = null      }     if  (vm.$el ) {       vm.$el .__vue__  = vm     }          if  (vm.$vnode  && vm.$parent  && vm.$vnode  === vm.$parent ._vnode ) {       vm.$parent .$el  = vm.$el      }             } 
 
我们可以看到,vm_patch执行时传入了两个参数,前一个参数代表旧的VNode,第二个参数代表新的VNode,比较旧VNode和新VNode的差异,和把VNode渲染成真实DOM,就是在patch中完成的。而patch函数的返回值,重新赋值到vm.$el上,由此可见,patch函数一般会传入vue挂载的根结点对应的Vdom。
来看看patch的算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 function  patch (oldVnode, vnode, hydrating, removeOnly ) {                 if  (isUndef (vnode)) {             if  (isDef (oldVnode)) invokeDestroyHook (oldVnode)             return          }         let  isInitialPatch = false          const  insertedVnodeQueue = []                           if  (isUndef (oldVnode)) {             isInitialPatch = true              createElm (vnode, insertedVnodeQueue)         } else  {                                       const  isRealElement = isDef (oldVnode.nodeType )             if  (!isRealElement && sameVnode (oldVnode, vnode)) {                                  patchVnode (oldVnode, vnode, insertedVnodeQueue, null , null , removeOnly)             } else  {                 if  (isRealElement) {                                                                                    if  (oldVnode.nodeType  === 1  && oldVnode.hasAttribute (SSR_ATTR )) {                         oldVnode.removeAttribute (SSR_ATTR )                         hydrating = true                      }                                                                 if  (isTrue (hydrating)) {                         if  (hydrate (oldVnode, vnode, insertedVnodeQueue)) {                             invokeInsertHook (vnode, insertedVnodeQueue, true )                             return  oldVnode                         } else  if  (process.env .NODE_ENV  !== 'production' ) {                             warn (                                 'The client-side rendered virtual DOM tree is not matching '  +                                 'server-rendered content. This is likely caused by incorrect '  +                                 'HTML markup, for example nesting block-level elements inside '  +                                 '<p>, or missing <tbody>. Bailing hydration and performing '  +                                 'full client-side render.'                              )                         }                     }                                                                                    oldVnode = emptyNodeAt (oldVnode)                 }                                                        const  oldElm = oldVnode.elm                  const  parentElm = nodeOps.parentNode (oldElm)                                                        createElm (                     vnode,                     insertedVnodeQueue,                                                                                    oldElm._leaveCb  ? null  : parentElm,                     nodeOps.nextSibling (oldElm)                 )                                       if  (isDef (vnode.parent )) {                     let  ancestor = vnode.parent                      const  patchable = isPatchable (vnode)                     while  (ancestor) {                         for  (let  i = 0 ; i < cbs.destroy .length ; ++i) {                             cbs.destroy [i](ancestor)                         }                         ancestor.elm  = vnode.elm                          if  (patchable) {                             for  (let  i = 0 ; i < cbs.create .length ; ++i) {                                 cbs.create [i](emptyNode, ancestor)                             }                                                                                                                    const  insert = ancestor.data .hook .insert                              if  (insert.merged ) {                                                                  for  (let  i = 1 ; i < insert.fns .length ; i++) {                                     insert.fns [i]()                                 }                             }                         } else  {                             registerRef (ancestor)                         }                         ancestor = ancestor.parent                      }                 }                                                        if  (isDef (parentElm)) {                     removeVnodes ([oldVnode], 0 , 0 )                 } else  if  (isDef (oldVnode.tag )) {                     invokeDestroyHook (oldVnode)                 }             }         }              invokeInsertHook (vnode, insertedVnodeQueue, isInitialPatch)         return  vnode.elm      } 
 
从代码中可以看出,patch比较新旧VNode时,会先判断新旧VNode是否为同一个节点,如果是同一个节点,调用patchVnode方法来继续比较,否则,直接删除旧节点,直接重新渲染一个新节点。
那么怎么样算同一个节点呢,我们来看看sameVNode的实现,可以看出来sameVNode返回true时,两个节点不一定相同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22                   function  sameVnode  (a, b) {  return  (     a.key  === b.key  &&     a.tag  === b.tag  &&     a.isComment  === b.isComment  &&     isDef (a.data ) === isDef (b.data ) &&     sameInputType (a, b)   ) } function  sameInputType  (a, b) {  if  (a.tag  !== 'input' ) return  true    let  i   const  typeA = isDef (i = a.data ) && isDef (i = i.attrs ) && i.type    const  typeB = isDef (i = b.data ) && isDef (i = i.attrs ) && i.type    return  typeA === typeB } 
 
然后我们看看 patchVnode 的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 function  patchVnode  (oldVnode, vnode, insertedVnodeQueue, removeOnly) {         if  (oldVnode === vnode) {       return      }          if  (isTrue (vnode.isStatic ) &&         isTrue (oldVnode.isStatic ) &&         vnode.key  === oldVnode.key  &&         (isTrue (vnode.isCloned ) || isTrue (vnode.isOnce ))) {       vnode.elm  = oldVnode.elm        vnode.componentInstance  = oldVnode.componentInstance        return      }     let  i     const  data = vnode.data      if  (isDef (data) && isDef (i = data.hook ) && isDef (i = i.prepatch )) {       i (oldVnode, vnode)     }     const  elm = vnode.elm  = oldVnode.elm      const  oldCh = oldVnode.children      const  ch = vnode.children      if  (isDef (data) && isPatchable (vnode)) {       for  (i = 0 ; i < cbs.update .length ; ++i) cbs.update [i](oldVnode, vnode)       if  (isDef (i = data.hook ) && isDef (i = i.update )) i (oldVnode, vnode)     }          if  (isUndef (vnode.text )) {       if  (isDef (oldCh) && isDef (ch)) {                  if  (oldCh !== ch) updateChildren (elm, oldCh, ch, insertedVnodeQueue, removeOnly)       } else  if  (isDef (ch)) {                  if  (isDef (oldVnode.text )) nodeOps.setTextContent (elm, '' )         addVnodes (elm, null , ch, 0 , ch.length  - 1 , insertedVnodeQueue)       } else  if  (isDef (oldCh)) {                  removeVnodes (elm, oldCh, 0 , oldCh.length  - 1 )       } else  if  (isDef (oldVnode.text )) {                  nodeOps.setTextContent (elm, '' )       }     } else  if  (oldVnode.text  !== vnode.text ) {              nodeOps.setTextContent (elm, vnode.text )     }          if  (isDef (data)) {       if  (isDef (i = data.hook ) && isDef (i = i.postpatch )) i (oldVnode, vnode)     }   } 
 
总结一下,patchVNode的规则是这样的
如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),那么只需要替换elm以及componentInstance即可。 
新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren,这个updateChildren也是diff的核心。 
如果老节点没有子节点而新节点存在子节点,先清空老节点DOM的文本内容,然后为当前DOM节点加入子节点。 
当新节点没有子节点而老节点有子节点的时候,则移除该DOM节点的所有子节点。 
当新老节点都无子节点的时候,只是文本的替换。 
 
你可能会疑惑patch和patchNode和updateChildren的区别,我们来看看他们调用的时机和实际的效果,patch中的patchNode是在确定新旧节点是同一个节点的情况下调用的
1 2 3 4 if  (!isRealElement && sameVnode (oldVnode, vnode)) {         patchVnode (oldVnode, vnode, insertedVnodeQueue, null , null , removeOnly) }  
 
patchVnode的用处有两个
把两个判定为sameVnode的节点进行进一步的比较和替换 
调用 updateChildren 比较子节点 
 
所以patchVNode仍然是在比较patch中的节点,只有 updateChildren 才是真正开始比较子节点。但是我一直搞不懂的是,如果sameVnode判定是true的话,两个节点应该基本属于一个相同的VNode类型,文字节点不可能有children属性,元素节点也不可能有text属性,所以3的情况,应该进不到patchVNode里…..事实上我调试了半天也没调试出这种情况…..暂且略过8,有空再回头看看
updateChildren 先上源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 function  updateChildren  (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {    let  oldStartIdx = 0      let  newStartIdx = 0      let  oldEndIdx = oldCh.length  - 1      let  oldStartVnode = oldCh[0 ]     let  oldEndVnode = oldCh[oldEndIdx]     let  newEndIdx = newCh.length  - 1      let  newStartVnode = newCh[0 ]     let  newEndVnode = newCh[newEndIdx]     let  oldKeyToIdx, idxInOld, elmToMove, refElm                    const  canMove = !removeOnly          while  (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {       if  (isUndef (oldStartVnode)) {         oldStartVnode = oldCh[++oldStartIdx]        } else  if  (isUndef (oldEndVnode)) {         oldEndVnode = oldCh[--oldEndIdx]       } else  if  (sameVnode (oldStartVnode, newStartVnode)) {         patchVnode (oldStartVnode, newStartVnode, insertedVnodeQueue)         oldStartVnode = oldCh[++oldStartIdx]         newStartVnode = newCh[++newStartIdx]       } else  if  (sameVnode (oldEndVnode, newEndVnode)) {         patchVnode (oldEndVnode, newEndVnode, insertedVnodeQueue)         oldEndVnode = oldCh[--oldEndIdx]         newEndVnode = newCh[--newEndIdx]       } else  if  (sameVnode (oldStartVnode, newEndVnode)) {          patchVnode (oldStartVnode, newEndVnode, insertedVnodeQueue)         canMove && nodeOps.insertBefore (parentElm, oldStartVnode.elm , nodeOps.nextSibling (oldEndVnode.elm ))         oldStartVnode = oldCh[++oldStartIdx]         newEndVnode = newCh[--newEndIdx]       } else  if  (sameVnode (oldEndVnode, newStartVnode)) {          patchVnode (oldEndVnode, newStartVnode, insertedVnodeQueue)         canMove && nodeOps.insertBefore (parentElm, oldEndVnode.elm , oldStartVnode.elm )         oldEndVnode = oldCh[--oldEndIdx]         newStartVnode = newCh[++newStartIdx]       } else  {         if  (isUndef (oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx (oldCh, oldStartIdx, oldEndIdx)         idxInOld = isDef (newStartVnode.key ) ? oldKeyToIdx[newStartVnode.key ] : null          if  (isUndef (idxInOld)) {            createElm (newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm )           newStartVnode = newCh[++newStartIdx]         } else  {           elmToMove = oldCh[idxInOld]                      if  (process.env .NODE_ENV  !== 'production'  && !elmToMove) {             warn (               'It seems there are duplicate keys that is causing an update error. '  +               'Make sure each v-for item has a unique key.'              )           }           if  (sameVnode (elmToMove, newStartVnode)) {             patchVnode (elmToMove, newStartVnode, insertedVnodeQueue)             oldCh[idxInOld] = undefined              canMove && nodeOps.insertBefore (parentElm, newStartVnode.elm , oldStartVnode.elm )             newStartVnode = newCh[++newStartIdx]           } else  {                          createElm (newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm )             newStartVnode = newCh[++newStartIdx]           }         }       }     }     if  (oldStartIdx > oldEndIdx) {       refElm = isUndef (newCh[newEndIdx + 1 ]) ? null  : newCh[newEndIdx + 1 ].elm        addVnodes (parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)     } else  if  (newStartIdx > newEndIdx) {       removeVnodes (parentElm, oldCh, oldStartIdx, oldEndIdx)     }   } 
 
变量指向如图
  
updateChildren对比子节点的方式是从中间到两边对比,也就是两边向中间处理节点,图中index和指向的变量映射如下。
oldStartIdx指向的节点是 oldStartVnode,旧VNode中未处理的第一个节点 
oldEndIdx指向的节点是oldEndVnode, 旧VNode中未处理的最后一个节点 
newStartIdx指向的节点是newStartVnode, 新VNode中未处理的第一个节点 
newEndIdx指向的节点是newEndVnode, 新VNode中未处理的最后一个节点 
 
遍历中,如果存在key,并且满足sameVnode,会将该DOM节点进行复用,否则则会创建一个新的DOM节点。
在大多数情况下,节点的变化情况不大,可以进行预测,每次循环中先使用这四种情况进行比对,就可以处理大部分的情况。
oldStartVnode和newStartVnode对比 
oldEndVnode和newEndVnode 对比 
oldStartVnode和newEndVnode 对比 
oldEndVnode和newStartVnode 对比 
 
如果调用sameVNode对比的结果是true,调用patchVnode方法继续比较,不过如果是3, 4为true的情况,那就证明新VNode中节点的位置发生了改变,还需要移动真实DOM,其中情况3会把 oldStartVnode 对应的真实DOM移动到所有未处理节点的后面,情况4会把 oldEndVnode 对应的真实DOM移动到所有未处理节点的前面。
如果以上情况均不符合,且 如果oldVNode数组中的节点有key属性(通过v-key设置),则通过createKeyToOldIdx会得到一个名为oldKeyToIdx的对象,这个对象的键是oldVNode的key属性,值是oldVNode在节点数组中的下标。从这个对象上可以直接找到是否有与newStartVnode一致key的旧的VNode节点, 如果两两个key相同的VNode满足sameVnode,patchVnode的同时会将这个真实DOM(elmToMove)移动到oldStartVnode对应的真实DOM的前面。
但如果没有设置key属性,虽然也会调用 createKeyToOldIdx会得到一个名为oldKeyToIdx的对象 ,但是这个对象上没有属性,所以不能作为map使用,vue就会使用就地替换策略,直接创建新的真实DOM用于替换。还有找不到一致key或者key相同但 sameVnode 不满足的时候,也会直接创建真实DOM,这就是为什么建议在v-for中使用v-key的原因,因为通过key生成对象,可以减少查找时间,并且可以对真实DOM进行移动操作,而不是创建新的节点用于就地替换。
来看看循环结束时的情况,因为循环能继续的条件是oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx,那么只要新旧节点数量不一致就会出现出现一边startIndex和oldIndex没有碰头的情况,这时候我们要去处理真实DOM中多余的节点或者添加真实DOM中不够的节点。
当结束时oldStartIdx > oldEndIdx,这说明了新的VNode节点比老的VNode节点多,也就是比真实DOM多,需要将剩下的(也就是新增的)VNode节点插入到真实DOM节点中去,此时通过调用addVnodes(批量调用createElm的接口)将这些节点加入到真实DOM中去。
  
当结束时newStartIdx > newEndIdx时, 新的VNode节点比老的VNode节点少,也就是比真实DOM多少, 需要将剩下的(也就是多余的)VNode节点删除,通过调用removeVnodes(批量调用浏览器api中的element.removeChild)将这些多余的真实DOM删除。