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


highlight: tomorrow-night-eighties

theme: cyanosis

前言

Vue 的 patch 过程分为上下两篇

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

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

更新过程

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

javascript 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函数

```javascript 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控制子组件是否渲染
  • 新老节点相同
  • 新老节点不同

至于第一种情况,和初次渲染流程相同,这里就不多赘述了。下面这种情况会走第一种逻辑 xml <template> <div> <cmp1 v-if="xxx">xxx</cmp1> <cmp2 v-else>yyy</cmp2> </div> </template> 当修改xxxfalse,会创建cmp2,此时 cmp2oldVNodenull。也就是说如果组件在初次渲染挂载过,在更新阶段就有oldVNode,反之没有

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

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

sameVnode函数

javascript 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函数

```javascript 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.updatei 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函数是怎么更新子节点的,其实这个就是一个递归过程,代码如下(可以直接看后面的解释部分就行)

```javascript 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. 如果上述都没命中,进入下面的逻辑

javascript 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.elm(oldCh[key].elm)插到 oldStartVnode.elm 前面;让newStartIdx往后一位,并更新newStartVnode的值
    • 如果不同,调用createElm创建节点,并插入到oldStartVnode.elm前面。让newStartIdx往后一位,并更新newStartVnode的值

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

javascript 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钩子函数 javascript 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函数如下

```javascript export function updateChildComponent ( vm: Component, // 子组件实例 propsData: ?Object, listeners: ?Object, parentVnode: MountedComponentVNode, // 组件 vnode renderChildren: ?Array ) { 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)不成立时,会执行下面的逻辑 ```javascript 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.destroyi } // 更新 组件vnode 的 elm ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.createi } const insert = ancestor.data.hook.insert if (insert.merged) { // 从 1 开始,因为第一个insert hook 是 mounted for (let i = 1; i < insert.fns.length; i++) { insert.fnsi } } } 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树,所以需要更新 xml <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 对象来获取对应节点,比遍历方式更快

javascript 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的优先级高于其他属性

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值