Vue 2.6 源码分析之旅【4】 —— diff 算法__Vue.js
发布于 4 年前 作者 banyungong 1338 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

上一章我们分析了 vue 2.6.x 的异步更新策略,弄清楚了以下几个问题:

  • vue 是在什么阶段进行视图更新的
  • vue 是在什么位置进行视图更新的
  • 实现 vue 异步更新的核心原理

而今天我们将分析 vue diff 算法,相信读完本文,将会对以下问题有初步的认知:

  • vue diff 算法发生在什么位置
  • vue diff 算法的核心原理
  • 为什么推荐 v-for 要加上 key

那么便开始吧(๑•̀ㅂ•́)و✧!


生成虚拟 DOM

这个名词相信大家已经非常熟悉了,这里就当简单复习一下了。

首先自然是概念:

所谓虚拟 DOM,其实就是 js 的对象,主要的目的是描述真实 dom 的结构和关系。

也就是说,虚拟 DOM 就是真实 DOM 的映射。

而这也是 vue 能做到跨平台的原因之一:完成虚拟 DOM 的操作之后,将其 patch 为真实 DOM 的过程中:

  • 目标是浏览器,则 patch 成浏览器结构,那么就能适应浏览器
  • 目标是小程序,则 patch 成小程序的结构,那么就能适应小程序
  • 目标是 app,则 patch 成 app 的结构,那么就能适应移动端

那么我们来看看 vue 源码中,关于虚拟 DOM 的操作吧。

结合前面将到的,我们先定位 updateComponent 中的 _render

/*
 * file: src/core/instance/render.js
 */

Vue.prototype._render = function (): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options

  if (_parentVnode) {
    vm.$scopedSlots = normalizeScopedSlots(_parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots)
  }

  // set parent vnode. this allows render functions to have access
  // to the data on the placeholder node.
  vm.$vnode = _parentVnode
  // render self
  let vnode
  try {
    // There's no need to maintain a stack because all render fns are called
    // separately from one another. Nested component's render fns are called
    // when parent component is patched.
    currentRenderingInstance = vm
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    handleError(e, vm, `render`)
    // return error render result,
    // or previous vnode to prevent render error causing blank component
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
      try {
        vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
      } catch (e) {
        handleError(e, vm, `renderError`)
        vnode = vm._vnode
      }
    } else {
      vnode = vm._vnode
    }
  } finally {
    currentRenderingInstance = null
  }
  // if the returned array contains only a single node, allow it
  if (Array.isArray(vnode) && vnode.length === 1) {
    vnode = vnode[0]
  }
  // return empty vnode in case the render function errored out
  if (!(vnode instanceof VNode)) {
    if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
      warn(
        'Multiple root nodes returned from render function. Render function ' + 'should return a single root node.',
        vm
      )
    }
    vnode = createEmptyVNode()
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}

上面的核心功能在这里:

vnode = render.call(vm._renderProxy, vm.$createElement)

通过 render 生成虚拟 DOM,在最后将其返回出去。

虚拟 DOM 转真实 DOM

这里我们来到 _update

/*
 * file: src/core/instance/lifecycle.js
 */
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
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    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.
}

上面的核心在于:

if (!prevVnode) {
  // initial render
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
  // updates
  vm.$el = vm.__patch__(prevVnode, vnode)
}

从注释可以看出,当初始化的时候,是没有 prevVnode 的,所以会走 if 分支,之后会走 else 更新分支。

其核心方法都是 __patch__

在第一章的分析中,入口文件有一行代码:

Vue.prototype.__patch__ = inBrowser ? patch : noop

这里安装了平台特有的 patch 方法,我们可以追踪一下:

var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules })

说明 createPatchFunction 方法将返回一个 patch 方法,那么我们来到它的返回值:

/*
 * file: src/core/vdom/patch.js
 */
return function patch(oldVnode, vnode, hydrating, removeOnly) {
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }

  let isInitialPatch = false
  const insertedVnodeQueue = []

  if (isUndef(oldVnode)) {
    // **empty mount (likely as component), create new root element**
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // patch existing root node
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
      if (isRealElement) {
        // mounting to a real element
        // check if this is server-rendered content and if we can perform
        // a successful hydration.
        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.'
            )
          }
        }
        // either not server-rendered, or hydration failed.
        // create an empty node and replace it
        oldVnode = emptyNodeAt(oldVnode)
      }

      // replacing existing element
      const oldElm = oldVnode.elm
      const parentElm = nodeOps.parentNode(oldElm)

      // create new node
      createElm(
        vnode,
        insertedVnodeQueue,
        // extremely rare edge case: do not insert if old element is in a
        // leaving transition. Only happens when combining transition +
        // keep-alive + HOCs. (#4590)
        oldElm._leaveCb ? null : parentElm,
        nodeOps.nextSibling(oldElm)
      )

      // update parent placeholder node element, recursively
      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)
            }
            // #6513
            // invoke insert hooks that may have been merged by create hooks.
            // e.g. for directives that uses the "inserted" hook.
            const insert = ancestor.data.hook.insert
            if (insert.merged) {
              // start at index 1 to avoid re-invoking component mounted hook
              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
}

初始化流程

对于上面的代码,我们先看看初始化的执行流程:

  • 首先我们可以从注释和判断知道:通常情况下并不会走 if (isUndef(oldVnode)) 这个分支,因为只有当 empty mount 的情况下,才会走这里
  • 接着,初始化的时候,oldVnode === vm.$el,所以是真实节点,即 isRealElement 为真,那么就会走到 if (isRealElement) 分支中
  • 紧接着就会通过 oldVnode = emptyNodeAt(oldVnode) 将生成 oldNode 对一个的虚拟 DOM
  • 随后通过 createElm 创建新的 DOM 树,并且把它放在老节点的旁边
  • 最后通过 removeVnodes 删除老节点

更新流程

在更新流程中,传入的 oldVnode 是 prevVnode,即这个时候它已经是一个虚拟 DOM 了,那么如果新老节点是同样类型的节点,就会走到:

patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)

而这就是大名鼎鼎的 diff 算法发生的地方。

在看 patchVnode 之前,我们先来看看 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)))
  )
}

可以看到,首先 key 一定要相同,然后节点类型相同,如果是 input,则 type 要相同,等等。

二者都没有 key 的时候,undefined === undefined

那么我们接着来到 patchVnode

function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
  if (oldVnode === vnode) {
    return
  }

  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  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
  }

  // reuse element for static trees.
  // note we only do this if the vnode is cloned -
  // if the new node is not cloned it means the render functions have been
  // reset by the hot-reload-api and we need to do a proper re-render.
  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
  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)
  }
}

这里总的来说,包括了三种操作:

  • 节点属性更新
  • 文本更新
  • 子节点更新

其中本文节点和子节点是互斥的,因为一个节点一旦是文本节点,那么它必然没有子节点,反之它一定不是文本节点。

具体的更新规则如下:

  • 新老节点都有子节点,则调用 updateChildren

    if (isDef(oldCh) && isDef(ch)) {
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    }
    
  • 新⽼节点都没有⼦节点,进行文本替换

    else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }
    
  • 新节点有⼦节点⽽⽼节点没有⼦节点,则先清空老节点的文本内容,然后为其新增子节点

    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, '')
    }
    

那么我们来看看 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

  // removeOnly is a special flag used only by <transition-group>
  // to ensure removed elements stay in correct relative positions
  // during leaving transitions
  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 {
          // same key but different element. treat as new element
          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 个对应的节点,分别代表老节点和新节点的首尾

  • 进行了一个循环操作,结束条件是老节点和新节点的游标重合,在循环中进行了如下操作:

    • 调整节点引用,保证其有值

    • 如果新老节点的首部相同,则对这两个节点进行 patch 操作,并且首部游标分别后移一位

      else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      }
      
    • 如果新老节点尾部相同,则对这两个节点进行 patch 操作,并且尾部游标分别前移一位

      else if (sameVnode(oldEndVnode, newEndVnode)) {
      	patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      	oldEndVnode = oldCh[--oldEndIdx]
      	newEndVnode = newCh[--newEndIdx]
      }
      
    • 如果老节点首部与新节点尾部相同,则对这两个节点进行 patch 操作,老节点首部游标加一,新节点尾部游标减一,并且还要做额外的移动操作,将老节点首部移动到尾部。这个操作其实并不难理解:因为满足这个条件的时候,很可能是进行了逆序的操作。

      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]
      }
      
    • 如果老节点尾部与新节点首部相同,则对这两个节点进行 patch 操作,老节点尾部游标减一,新节点首部游标加一,并且还要做额外的移动操作,将老节点尾部移动到首部。

      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]
      }
      
    • 如果上述条件都不满足,则只能进行效率较为低下的遍历操作:在老节点中找是否存在于新节点首部相同的节点,这里又存在两种情况:

      • 没找到:说明这个节点是新增节点,直接创建一个新节点
      • 找到了:
        • 相同节点:进行 patch 操作,并且移动老节点
        • 不同节点:创建新节点
  • 当循环结束之后,如果两组节点如果数量不同,则进入下面的流程:

    • 老节点先结束,说明有新增节点,进行批量创建

      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)
      }
      

这里的逻辑不可谓不复杂了,我们将脑图整理出来再看看:

从上面我们可以总结出一些规律:

  • 节点比较一定是在同层级之间进行:同层比较
  • 如果存在子节点,一定会优先获取子节点:深度优先

一些有意思的东西

关于 vue 的虚拟 DOM

这个从 patch 文件的头部我们可以看到:

Virtual DOM patching algorithm based on Snabbdom by

说明 vue 中的虚拟 DOM 是基于 Snabbdom 的。

视图更新中诡异的画面

我们将断点断在新树生成,老树删除之间,可以看到这样的画面:

vfor 中 key 的作用

要说明这个问题,我们先来看看有 key 的情况下,diff 是如何进行的:

<div id="demo">
  <p v-for="item in arr" :key="item">{{a}}</p>
</div>
<script>
	const app = new Vue({
	    el: '#demo',
	    data: { arr: ['A','B','C','D'] },
	    mounted() {
	        setTimeout(() => {
	            this.arr.splice(1, 0, 'E')
	        }, 1000);
	    }
	});
</script>

首先,我们很清楚,数组最后会变成 ['A', 'E', 'B', 'C', 'D']

那么我们开始模拟 diff 流程:

  • Step 1
    • 老:['A', 'B', 'C', 'D']
    • 新:['A', 'E', 'B', 'C', 'D']
    • 比较二者头部,直接 patch
    • 结果:['A', 'B', 'C', 'D']
  • Step 2
    • 老:['B', 'C', 'D']
    • 新:['E', 'B', 'C', 'D']
    • 比较二者头部,不同节点 → 比较二者尾部,直接 patch
    • 结果:['A', 'B', 'C', 'D']
  • Step 3
    • 老:['B', 'C']
    • 新:['E', 'B', 'C']
    • 比较二者头部,不同节点 → 比较二者尾部,直接 patch
    • 结果:['A', 'B', 'C', 'D']
  • Step 4
    • 老:['B']
    • 新:['E', 'B']
    • 比较二者头部,不同节点 → 比较二者尾部,直接 patch
    • 结果:['A', 'B', 'C', 'D']
  • Step 5
    • 老:[]
    • 新:['E']
    • 老节点遍历结束,新节点还有剩余,新增节点,位置在 oldStartIdx 后,更新一次
    • 结果:['A', 'E', 'B', 'C', 'D']

综上,上面的算法总共更新了一次页面。

那么,如果没有 key,将会是什么样的情况呢?

  • Step 1
    • 老:['A', 'B', 'C', 'D']
    • 新:['A', 'E', 'B', 'C', 'D']
    • 比较二者头部,直接 patch
    • 结果:['A', 'B', 'C', 'D']
  • Step 2
    • 老:['B', 'C', 'D']
    • 新:['E', 'B', 'C', 'D']
    • 比较二者头部,直接 patch,更新一次
    • 结果:['A', 'E', 'C', 'D']
  • Step 3
    • 老:['C', 'D']
    • 新:['B', 'C', 'D']
    • 比较二者头部,直接 patch,更新两次
    • 结果:['A', 'E', 'B', 'D']
  • Step 4
    • 老:['D']
    • 新:['C', 'D']
    • 比较二者头部,直接 patch,更新三次
    • 结果:['A', 'E', 'B', 'C']
  • Step 5
    • 老:[]
    • 新:['D']
    • 老节点遍历结束,新节点还有剩余,新增节点,位置在 oldStartIdx 后,更新四次
    • 结果:['A', 'E', 'B', 'C', 'D']

这里仅仅是这么简单的一个数组,没有 key 的情况下,就已经如此繁琐,其效率之低下已经不言而喻。

这也说明了为什么 vue 非常推荐在使用 v-for 指令的时候为遍历的子项添加上 key,因为这样的效率远远高于没有 key 的情况。

结语

更佳阅读体验:Vue 2.6 源码分析之旅 - 四:diff 算法

那么今天的分析就到此为止啦,我们下期再见,咕咕咕~~~

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 初心Yearth 原文链接:https://juejin.im/post/6862955082237870087

回到顶部