Vue 源码(六)patch 过程(二)

1,044 阅读4分钟

前言

Vue 的 patch 过程分为上下两篇

通过这篇文章可以了解如下内容

  • Vue 更新过程
  • Vue 的 diff 算法
  • diff 算法的时间复杂度
  • v-forkey的作用

更新过程

修改响应式属性时,会通知订阅Watcher更新,从而触发组件重新渲染;首先还是执行组件render函数获取组件VNode,然后执行_update

  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el // dom 节点
    // 获取更新前的VNode
    const prevVnode = vm._vnode
    // 设置 activeInstance 并返回一个匿名函数,匿名函数返回值是上一个 activeInstance 的值
    const restoreActiveInstance = setActiveInstance(vm)
    // 当前 Vue 实例的 render 函数创建的 VNode
    vm._vnode = vnode
    if (!prevVnode) {
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // 更新
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    // 将 activeInstance 的值设置成上一个 vm 实例
    restoreActiveInstance()
    // update __vue__ reference
    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
    }
  }

相比于初次渲染,更新过程中的prevVnode是有值的(如果是通过v-if控制prevVnode没有值),值为更新前的VNode;所以会走else逻辑,else逻辑也是调用vm.__patch__函数,但是会传入prevVnode。注意这里会将vm._vnode设置成最新的VNode;

接着看patch函数

return 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) {
          oldVnode = emptyNodeAt(oldVnode)
        }

        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // 更新 组件vnode 的 elm 并重新执行父组件的 cbs.create 和 insert hooks(不包含 mounted 钩子)
        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)
            }
            // 更新 组件vnode 的 elm
            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) {
                // 从 1 开始,因为第一个insert hook 是 mounted
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

更新过程中,patch函数会出现3种情况,分别是:

  • 当前组件第一次创建,比如父组件通过v-if控制子组件是否渲染
  • 新老节点相同
  • 新老节点不同

至于第一种情况,和初次渲染流程相同,这里就不多赘述了。下面这种情况会走第一种逻辑

<template>
    <div>
        <cmp1 v-if="xxx">xxx</cmp1>
        <cmp2 v-else>yyy</cmp2>
    </div>
</template>

当修改xxxfalse,会创建cmp2,此时 cmp2oldVNodenull。也就是说如果组件在初次渲染挂载过,在更新阶段就有oldVNode,反之没有

而第二三种情况根据下面的逻辑判断

// patch 函数内部
// oldVnode 不是真实节点,并且 sameVnode 返回 true
if (!isRealElement && sameVnode(oldVnode, vnode)) {
  patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}

sameVnode函数

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)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}
  • 对于同步组件,如果两个 vnodekey 不相等,则不同;如果key相同则继续判断 isCommentdatainput 类型等是否相同
  • 对于异步组件,如果两个 vnodekey 不相等,则不同;如果key相同则继续判断asyncFactory是否相同

新老节点相同

!isRealElement && sameVnode(oldVnode, vnode)成立,会执行patchVnode函数

function patchVnode (
 oldVnode,
 vnode,
 insertedVnodeQueue,
 ownerArray,
 index,
 removeOnly
) {
  if (oldVnode === vnode) {
    return
  }
  // 将 oldVnode.elm 赋值给 vnode.elm
  const elm = vnode.elm = oldVnode.elm

  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
    } else {
      vnode.isAsyncPlaceholder = true
    }
    return
  }
  if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
     ) {
    vnode.componentInstance = oldVnode.componentInstance
    return
  }

  let i
  const data = vnode.data
  // 这里需要注意:组件占位符 VNode 的 Vnode.children 属性始终为空
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }

  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 (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(ch)
      }
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {
      removeVnodes(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的elm属性,也就是说接下来的过程其实就是修改DOM树的过程

他的逻辑是,如果新老节点的 VNode 相同则返回。反之,将 oldVnode.elm 赋值给 vnode.elm;如果VNode是组件占位符VNode,执行VNode的prepatch钩子函数去更新子组件;然后,获取新老节点的子节点;执行cbs.update里面的所有函数和VNode的update钩子函数,全量更新节点的所有属性;然后开始比对,如果新节点是文本节点且新旧文本不相同,则直接替换elm文本内容。如果新VNode不是文本节点,则判断它们的子节点,并分了几种情况处理:

  1. 新老节点都有子节点,并且子节点不相同,使用updateChildren函数来更新子节点
  2. 如果只有新VNode有子节点,说明老节点要么是文本节点要么就是没有子节点;如果旧的节点是文本节点将节点的文本清除;然后通过 addVnodes 将新VNode的所有子节点批量插入到新节点 elm
  3. 如果只有老VNode有子节点,说明新VNode是空节点;则将老VNode的所有子节点通过removeVnodes全部清除
  4. 当只有旧节点是文本节点的时候,则清除其节点文本内容

上述执行完之后,会执行postpatch钩子函数

updateChildren

上面第一条中当新老节点都有子节点,并且子节点不相同时会调用updateChildren函数;看下updateChildren函数是怎么更新子节点的,其实这个就是一个递归过程,代码如下(可以直接看后面的解释部分就行)

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, vnodeToMove, refElm

  const canMove = !removeOnly

  if (process.env.NODE_ENV !== 'production') {
    checkDuplicateKeys(newCh)
  }
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } 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]
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      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]
      : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      if (isUndef(idxInOld)) { // New element
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
      } else {
        vnodeToMove = oldCh[idxInOld]
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
          oldCh[idxInOld] = undefined
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        }
      }
      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(oldCh, oldStartIdx, oldEndIdx)
  }
}

首先会定义4个指针以及4个指针对应的VNode节点

  • oldStartIdxoldEndIdxnewStartIdxnewEndIdx分别是新老两个VNode的两边索引
  • oldStartVnodeoldEndVnodenewStartVnodenewEndVnode分别指向这几个索引对应的VNode节点

然后是一个while循环,oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx;在这个过程中4个指针会逐渐向中间靠拢,直到老节点的开始索引大于老节点的结束索引或者新节点的开始索引大于新节点的结束索引时,while循环结束。

while循环内的逻辑如下

  1. oldStartVnode 没有的情况,oldStartIdx向中间靠拢,并更新oldStartVnode的值
  2. oldEndVnode 没有的情况,oldEndIdx向中间靠拢,并更新oldEndVnode的值
  3. oldStartVnodenewStartVnode 是相同节点,也就是两个节点的开头是相同的,调用patchVnode去更新子节点,子节点更新完成之后,将 oldStartIdxnewStartIdx 向后移动一位
  4. oldEndVnodenewEndVnode 是相同节点,也就是两个节点的结尾是相同的,同样进行 patchVnode 操作并将 oldEndIdxnewEndVnode 向前移动一位
  5. oldStartVnodenewEndVnode 是相同节点,也就是老节点的头部与新节点的尾部是同一节点时,调用patchVnode,更新子节点;子节点更新完成之后,将 oldStartVnode.elm移动到 oldEndVnode.elm 后面;然后 oldStartIdx 向后移动一位,newEndIdx 向前移动一位。

diff1.jpg

  1. oldEndVnodenewStartVnode 是相同节点,也就是老节点的尾部与新节点的头部是同一节点的时候,调用patchVnode,更新子节点;子节点更新完成之后,将 oldEndVnode.elm 插入到 oldStartVnode.elm 前面;oldEndIdx 向前移动一位,newStartIdx 向后移动一位。

diff2.jpg

  1. 如果上述都没命中,进入下面的逻辑
 else {
   if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
   idxInOld = isDef(newStartVnode.key)
     ? oldKeyToIdx[newStartVnode.key]
   : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
   if (isUndef(idxInOld)) {
     createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
   } else {
     vnodeToMove = oldCh[idxInOld]
     if (sameVnode(vnodeToMove, newStartVnode)) {
       patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
       oldCh[idxInOld] = undefined
       canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
     } else {
       createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
     }
   }
   newStartVnode = newCh[++newStartIdx]
 }

首先通过createKeyToOldIdx获取oldCh中所有key{ [key的名字]: [在oldCh中的索引] };如果newStartVnodekey,获取这个 keyoldCh中的位置,并赋值给idxInOld;否则,遍历 oldCh ,查找和 newStartVnode相同的节点,如果找到了就返回对应的索引,并赋值给idxInOld。接下来逻辑如下:

  • 如果idxInOldundefined,说明newStartVnodeoldCh中所有节点都不相同,调用createElm创建节点,并插入到oldStartVnode.elm前面。让newStartIdx往后一位,并更新newStartVnode的值

  • 如果idxInOld有值,说明newStartVnodeoldCh中有一样的节点或者相同节点,获取这个节点,并再次通过sameVnode判断这个节点和newStartVnode是否相同

    • 如果相同调用patchVnode更新子节点,子节点更新完成后将这个节点从oldCh中删除,并将 vnodeToMove.elmoldCh[key].elm)插到 oldStartVnode.elm 前面;让newStartIdx往后一位,并更新newStartVnode的值
    • 如果不同,调用createElm创建节点,并插入到oldStartVnode.elm前面。让newStartIdx往后一位,并更新newStartVnode的值

while循环结束会执行下面逻辑

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(oldCh, oldStartIdx, oldEndIdx)
}
  • 如果oldStartIdx大于oldEndIdx说明老节点先遍历完成。然后判断newCh[newEndIdx + 1]是否有值,如果有值说明剩余的新节点(newStartIdxnewEndIdx之间的节点)应该插入到newCh[newEndIdx + 1].elm前面;反之插入到最后
  • 如果newStartIdx大于newEndIdx说明新节点先遍历完成。直接将oldStartIdxoldEndIdx之间的所有节点全部删除

prepatch钩子函数

patchVnode方法中,如果新VNode是组件占位符VNode,会调用VNode的prepatch钩子函数

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    // 获取组件占位符VNode的 options
    const options = vnode.componentOptions
    // 获取组件实例
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props 传入子组件的最新的 props 值
      options.listeners, // updated listeners 自定义事件
      vnode, // new parent vnode
      options.children // new children
    )
  }

prepatch钩子函数内调用updateChildComponent,并传入子组件实例、最新的prop数据、自定义事件、新VNode和子节点(通过name属性指定插槽内容的具名插槽)

updateChildComponent函数如下

export function updateChildComponent (
  vm: Component, // 子组件实例
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode, // 组件 vnode
  renderChildren: ?Array<VNode>
) {
  if (process.env.NODE_ENV !== 'production') {
    // 设置成 true 的目的是,给 props[key] 赋值时,触发 set 方法,不会让 customSetter 函数报错
    isUpdatingChildComponent = true
  }

  const newScopedSlots = parentVnode.data.scopedSlots
  const oldScopedSlots = vm.$scopedSlots
  const hasDynamicScopedSlot = !!(
    (newScopedSlots && !newScopedSlots.$stable) ||
    (oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
    (newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key)
  )


  const needsForceUpdate = !!(
    renderChildren ||               // has new static slots
    vm.$options._renderChildren ||  // has old static slots
    hasDynamicScopedSlot
  )
  // vm.$options._parentVnode 指向 新的 组件vnode
  vm.$options._parentVnode = parentVnode
  // vm.$vnode 指向 新的 组件vnode
  vm.$vnode = parentVnode // update vm's placeholder node without re-render

  if (vm._vnode) { // update child tree's parent
    // 更新 渲染vnode 的 parent
    vm._vnode.parent = parentVnode
  }
  vm.$options._renderChildren = renderChildren

  vm.$attrs = parentVnode.data.attrs || emptyObject
  vm.$listeners = listeners || emptyObject

  // update props
  // 更新 props
  if (propsData && vm.$options.props) {
    toggleObserving(false)
    // 之前的 propsData
    const props = vm._props
    // 子组件定义的 props 的属性集合
    const propKeys = vm.$options._propKeys || []
    for (let i = 0; i < propKeys.length; i++) {
      const key = propKeys[i]
      const propOptions: any = vm.$options.props // wtf flow?
      // 在这里修改props的值触发 组件更新
      props[key] = validateProp(key, propOptions, propsData, vm)
    }
    toggleObserving(true)
    vm.$options.propsData = propsData
  }

  // update listeners
  listeners = listeners || emptyObject
  // 获取上一次绑定的自定义事件
  const oldListeners = vm.$options._parentListeners
  // 将此次的自定义事件赋值给 _parentListeners
  vm.$options._parentListeners = listeners
  updateComponentListeners(vm, listeners, oldListeners)

  // resolve slots + force update if has children
  if (needsForceUpdate) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
  }

  if (process.env.NODE_ENV !== 'production') {
    // 更新完成后,置为 false
    isUpdatingChildComponent = false
  }
}

由于更新了VNode,那么VNode对应实例的一系列属性也会发生变化,包括vm.$vnode的更新、slot的更新,listeners的更新,props的更新等等。这些属性的更新会触发子组件更新,具体更新方式在对应文章中都会介绍,这里就不赘述了。

如果子组件需要更新,则将子组件的Render Watcher直接添加到正在执行的队列中等待执行,而不是调用nextTick,因为此时queueWatcher方法的flushingtrue,会将子组件的Render Watcher添加到队列的正确位置上;waitingtrue,不会调用nextTick方法。

添加到队列后,回到patchVnode函数,继续更新;当父组件更新完成后,根据队列中的顺序,更新子组件。

新老节点不同

!isRealElement && sameVnode(oldVnode, vnode)不成立时,会执行下面的逻辑

if (!isRealElement && sameVnode(oldVnode, vnode)) {
  // patch existing root node
  patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
  if (isRealElement) {
    // 根据 oldVnode(此时 oldVnode 是真实节点) 创建一个 vnode
    oldVnode = emptyNodeAt(oldVnode)
  }
  // 获取节点的 真实元素
  const oldElm = oldVnode.elm
  // 获取 oldVnode 的 父节点
  const parentElm = nodeOps.parentNode(oldElm)

  createElm(
    vnode,
    insertedVnodeQueue,
    oldElm._leaveCb ? null : parentElm,
    nodeOps.nextSibling(oldElm)
  )

  // 更新 组件vnode 的 elm 并重新执行 cbs.create 和 父组件的 insert hooks(不包含 mounted 钩子)
  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)
      }
      // 更新 组件vnode 的 elm
      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) {
          // 从 1 开始,因为第一个insert hook 是 mounted
          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)
  }
}

和第一次创建相同,通过createElm创建节点,如果是组件占位符VNode,则调用init钩子函数创建组件实例,并执行组件的挂载过程。如果是普通VNode,创建节点,并调用createChildren创建子节点,将子节点插入到当前节点中。上述执行完成后,将当前节点插入父节点中。

回到patch函数,更新组件占位符VNode的elm属性,并重新执行cbs.create内的函数和组件占位符VNode的insert钩子函数(不包含mounted的钩子),最后 删除旧节点,返回最新的DOM树,并赋值给vm.$el

执行insert钩子的目的是防止出现下面这种情况,如果在组件占位符VNode上有自定义指令,并且insert回调内绑定了DOM,如果不更新,一直绑定的是老DOM树,所以需要更新

<template>
    <div v-if="xxx">xxx</div>
    <div v-else>yyy</div>
</template>

总结

Vue 的 diff 算法

  1. 同级比较,然后再去比较子节点
  2. 判断一方有子节点一方没有子节点的情况
  3. 比较都有子节点的情况
  4. 递归比较子节点,具体流程如下

创建4个指针,分别指向新老VNode孩子节点数组的头尾;通过while循环,遍历数组并垂直、交叉比较

  • 如果比较的VNode 相同,则比较该VNode的孩子节点;并修改两个指针的位置。
  • 如果垂直、交叉比较没有命中,查看老VNode数组中有没有和新VNode数组的头指针指向的VNode相同的VNode,如果有,比对该VNode的孩子节点。比对完成之后删除老DOM,将新DOM插入到老VNode数组头指针指向的VNode对应DOM的前面;并移动新VNode数组的头指针
  • 直至老VNode数组的头指针大于尾指针;或者新VNode数组的头指针大于尾指针
    • 老VNode数组的头指针大于尾指针:说明新增了一些DOM。判断新VNode数组尾指针+1是否还有VNode,如果有将这些DOM插入到这个VNode对应的DOM之前;反之插入最后
    • 新VNode数组的头指针大于尾指针:说明还有多余的DOM,删除这些DOM

时间复杂度

时间复杂度是O(n),只比较同级不考虑跨级问题

v-forkey的作用

如果不使用key,Vue 会尽可能的就地修改/复用相同类型元素的算法。key是 Vue 中 VNode 的唯一标记,通过这个key, diff 操作可以更准确、更快速

更准确:因为带key不是就地复用了,在sameNode函数a.key === b.key对比中可以避免就地复用的情况,所以会更加准确

更快速:利用key的唯一性生成 map 对象来获取对应节点,比遍历方式更快

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)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

可以看到,key的优先级高于其他属性