<深入浅出Vuejs>虚拟DOM_Patch()

        虚拟DOM最核心的部分是Patch方法,它通过对比新旧两个Vnode之间有哪些不同,然后根据对比结果找出需要更新的节点进行更新.当oldVnode和Vnode不相同时,以Vnode为准来渲染视图.

        而Patch中使用了Diff算法来进行比较.Diff 算法是一种通过同层的树节点进行比较的高效算法,避免了对树进行逐层搜索遍历,所以时间复杂度只有 O(n)         

让我们先来看看VUE初始化时最后的工作:vm.$mount将vm实例挂载到DOM元素上.

 Vue.prototype._init = function (options?: Object) {
    ......

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

        而vue.$mount()函数最后会调用lifestyle.js中的mountComponent方法:其中定义了一个updateComponent函数用于更新组件.

        然后对这个Vue实例(组件)新建一个Watcher并完成依赖收集工作,当响应式数据发生变化时,它的dep数组便会调用dep.notify()来通知所有的依赖更新视图,此时就会调用updateComponent方法.

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
    //el代表DOM节点
  vm.$el = el
    ......
  //钩子函数
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
       ...
    }
  } else {
    updateComponent = () => {
      // vm._render() 返回一个VNode,在生成VNode的过程中,会动态计算getter,同时推入到dep里                       
      vm._update(vm._render(), hydrating)
    }
  }
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

         updateComponent(): updateComponent()执行的时候内部会调用,vm.__update()方法,并将_render()函数产生的新Vnode传入其中,与OldVnode做diff比较,最后完成更新工作.而_update()函数的定义如下:

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode    //由render产生的新的Vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    //如果不存在OldVnode,就使用新Vnode创建一个真实DOM
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      //如果存在OldVnode,就调用patch将OldVnode与Vnode做diff操作,将需要更新的Dom节点进行更新.
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

        __update:__update()中调用了__patch__函数.

     综上,patch不是暴力替换节点,而是修改DOM节点(也可以理解为渲染视图),需要做三件事:

1.创建新增的节点  (发生于OldVnode不存在而Vnode存在时,或Vnode和oldVnode不是同一个节点)

2.删除废弃节点  (发生于节点只存在于OldVnode中)  

3.修改需要更新的节点(当两个节点时相同的节点时,做Diff比较,更新不同的节点)        

        __patch__:那么vm.__patch__函数中究竟发生了什么呢?

function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
        // 当oldVnode不存在时
        if (isUndef(oldVnode)) {
            // 创建新的节点
            createElm(vnode, insertedVnodeQueue, parentElm, refElm)
        } else {
            const isRealElement = isDef(oldVnode.nodeType)
            if (!isRealElement && sameVnode(oldVnode, vnode)) {
            // 对oldVnode和vnode进行diff,并对oldVnode打patch
                patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
            } 
    }

//sameNode
function sameVnode (a, b) {
  return (
    a.key === b.key &&
    a.asyncFactory === b.asyncFactory && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

        如果不存在oldVnode,就直接用新Vnode创建一个新节点,否则调用sameVnode函数判断OldVnode和Vnode是不是同一个节点(通过判断基本属性),如果是则调用Diff,否则直接跳过Diff过程,根据Vnode创建新的DOM节点,然后删除OldVnode的DOM节点.

        patchVnode:我们着重分析更新节点过程,即Diff过程.对应的方法为patchVnode()(Patch.js)

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  // 如果 oldVnode、vnode 是同一节点的话,则直接 return 即可,不同进行更新操作
  if (oldVnode === vnode) {
    return
  }
 
  // 获取对比 vnode 对应的真实的 DOM 节点
  const elm = vnode.elm = oldVnode.elm
 
  // 判断 oldVnode 和 vnode 是不是静态节点,如果是静态节点的话,直接 return
  if (isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance
    return
  }
 
  // 获取新旧 vnode 的 children
  const oldCh = oldVnode.children
  const ch = vnode.children
  if (isUndef(vnode.text)) {//Vnode是元素节点或者text为空的文本节点
    if (isDef(oldCh) && isDef(ch)) {//Vnode和OldVnode都是元素节点,且都有子节点,更新子节点
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) {//Vnode和OldVnode都是元素节节点,但只有Vnode有子节点,添加子节点
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {//Vnode和OldVnode都是元素节点,但只有OldVnode有子节点,删除子节点
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {//Vnode和OldVnode都是文本节点,但只有oldVnode有text,删除text
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {//Vnode有text属性,替换文本
    nodeOps.setTextContent(elm, vnode.text)
  }
}

         updateChildren():我们继续分析,更新子节点函数updateChildren(),它是diff中最重要的环节.

         直观上对子节点的更新方法是:循环 newChildren(新子节点列表),每循环到一个新的子节点,就去 oldChildren(旧的子节点列表)中循环查找与其相同的那个旧节点。如果在 oldChildren 中找不到的话,则说明当前循环的新子节点是新增节点,此时需要进行创建新节点并插入到视图的操作。如果找到了与当前循环节点相同的那个旧节点,此时需要进行更新节点的操作。另外还需要说明的一点是,如果找到了与当前循环节点相同的那个旧节点,但是位置不一样的话,除了做更新操作,还需要对节点的位置进行移动。当newChildren中的所有节点遍历完成,oldChildren中仍有剩余,则删除这些节点.

        综上,对子节点的更新包含四个操作:

        新增节点:插入未处理节点的前面
        删除节点:newChildren遍历完成后,删除oldChildren中剩余的节点.
        更新节点(同节点位置相同):与前文所述的更新节点操作一致
        移动节点(同节点位置不同):更新,并把节点已知未处理节点最前面

       Diff: 但这不是性能最优的解决方案,为此引入Diff算法来提高性能.他有两个特点:

        1.比较只会在同层进行,不会跨层级比较

        2.循环从两边向中间进行

        1.第一步

        对oldChildren和newChildren的开始和结束位置进行标记:oldStartIdx、oldEndIdx、newStartIdx、newEndIdx。

let oldStartIdx = 0 // 旧节点开始下标
let newStartIdx = 0 // 新节点开始下标
let oldEndIdx = oldCh.length - 1 // 旧节点结束下标
let oldStartVnode = oldCh[0]  // 旧节点开始vnode
let oldEndVnode = oldCh[oldEndIdx] // 旧节点结束vnode
let newEndIdx = newCh.length - 1 // 新节点结束下标
let newStartVnode = newCh[0] // 新节点开始vnode
let newEndVnode = newCh[newEndIdx] // 新节点结束vnode

         2.第二步

        进入循环处理,分情况讨论并移动对应的VNode节点,退出条件是(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    ....//处理逻辑
}

 当newStartVnode == oldStartVnode时:

 if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
     }

 当newEndVnode == oldEndVnode时:

else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      }

 以上两种情况直接调用patchVnode()完成更新节点逻辑,并且移动指针.

当oldStartVnode == newEndedVnode时:将节点移动到未处理节点最后面.

else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      }

当oldEndVnode == newStartVnode时:将节点移动到未处理节点最前面.

else if(sameVnode(oldEndVnode,newStartVnode))
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      }

        如此,已更新过的节点无论是内容还是位置都是正确的,更新完后就不需要在进行考虑了,我们只需要在未处理节点([start,end])之间进行移动和更新操作即可.

        以上四种情况均不满足时:

else {// 没有找到相同的可以复用的节点,则新建节点处理
        /* 生成一个key与旧VNode的key对应的哈希表(只有第一次进来undefined的时候会生成,也为后面检测重复的key值做铺垫) 比如childre是这样的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}] beginIdx = 0 endIdx = 2 结果生成{key0: 0, key1: 1, key2: 2} */
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        /*如果newStartVnode新的VNode节点存在key并且这个key在oldVnode中能找到则返回这个节点的idxInOld(即第几个节点,下标)*/
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          /*newStartVnode没有key或者是该key没有在老节点中找到则创建一个新的节点*/
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          /*获取同key的老节点*/
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            /*如果新VNode与得到的有相同key的节点是同一个VNode则进行patchVnode*/
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            //因为已经patchVnode进去了,所以将这个老节点赋值undefined
            oldCh[idxInOld] = undefined
            /*当有标识位canMove实可以直接插入oldStartVnode对应的真实Dom节点前面*/
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            /*当新的VNode与找到的同样key的VNode不是sameVNode的时候(比如说tag不一样或者是有不一样type的input标签),创建一个新的节点*/
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }

        大部分情况下,前面四种方式就可以找到相同的节点,所以节省了很多次循环操作.

        3.第三步 

        退出While循环时,如果newChildren有剩余,则将剩余的节点都加入真实的DOM,如果oldChildren有剩余,则把剩余的节点都删除.至此,整个diff过程完成.

 if (oldStartIdx > oldEndIdx) {
      /*全部比较完成以后,发现oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多, 所以这时候多出来的新节点需要一个一个创建出来加入到真实Dom中*/
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) //创建 newStartIdx - newEndIdx 之间的所有节点
    } else if (newStartIdx > newEndIdx) {
      /*如果全部比较完成以后发现newStartIdx > newEndIdx,则说明新节点已经遍历完了,老节点多于新节点,这个时候需要将多余的老节点从真实Dom中移除*/
      removeVnodes(oldCh, oldStartIdx, oldEndIdx) //移除 oldStartIdx - oldEndIdx 之间的所有节点
    }

小结:

1.本文探讨了Vue何时执行更新DOM元素(updateComponent)的具体流程.

2.本文探讨了通过如何比较Vnode和oldVnode,以及修改DOM的三种情况的逻辑(patch函数)

3.本文探讨了更新节点的详细逻辑(patchVnode)

4.本文探讨了更新子节点的详细逻辑(diff算法)

        本文章仅为本人学习总结,如果有不足还请各位指出!

 

       


 

 

 

        

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值