详解vue3.0 diff算法的使用(超详细)

前言:随之vue3.0beta版本的发布,vue3.0正式版本相信不久就会与我们相遇。尤玉溪在直播中也说了vue3.0的新特性typescript强烈支持,proxy响应式原理,重新虚拟dom,优化diff算法性能提升等等。小编在这里仔细研究了vue3.0beta版本diff算法的源码,并希望把其中的细节和奥妙和大家一起分享。

第一次patchChidren

第三次patchChidren‘

如果没有用到diff算法,而是依次patch虚拟dom树,那么如上稍微 修改dom顺序 ,就会在patch过程中没有一对正确的新老vnode,所以老vnode的节点没有一个可以复用,这样就需要重新创造新的节点,浪费了性能开销,这显然不是我们需要的。

那么diff算法的作用就来了。

diff作用就是在patch子vnode过程中,找到与新vnode对应的老vnode,复用真实的dom节点,避免不必要的性能开销

二 diff算法具体做了什么(重点)?

在正式讲diff算法之前,在patchChildren的过程中,存在 patchKeyedChildren

patchUnkeyedChildren

patchKeyedChildren 是正式的开启diff的流程,那么patchUnkeyedChildren的作用是什么呢? 我们来看看针对没有key的情况patchUnkeyedChildren会做什么。

c1 = c1 || EMPTY_ARR
  c2 = c2 || EMPTY_ARR
  const oldLength = c1.length
  const newLength = c2.length
  const commonLength = Math.min(oldLength, newLength)
  let i
  for (i = 0; i < commonLength; i++) { /* 依次遍历新老vnode进行patch */
   const nextChild = (c2[i] = optimized
    ? cloneIfMounted(c2[i] as VNode)
    : normalizeVNode(c2[i]))
   patch(
    c1[i],
    nextChild,
    container,
    null,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
   )
  }
  if (oldLength > newLength) { /* 老vnode 数量大于新的vnode,删除多余的节点 */
   unmountChildren(c1, parentComponent, parentSuspense, true, commonLength)
  } else { /* /* 老vnode 数量小于于新的vnode,创造新的即诶安 */
   mountChildren(
    c2,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized,
    commonLength
   )
  }

我们可以得到结论,对于不存在key情况

① 比较新老children的length获取最小值 然后对于公共部分,进行从新patch工作。

② 如果老节点数量大于新的节点数量 ,移除多出来的节点。

③ 如果新的节点数量大于老节点的数量,从新 mountChildren新增的节点。

那么对于存在key情况呢? 会用到diff算法 , diff算法做了什么呢?

patchKeyedChildren方法究竟做了什么?

我们先来看看一些声明的变量。

  /* c1 老的vnode c2 新的vnode */
  let i = 0       /* 记录索引 */
  const l2 = c2.length  /* 新vnode的数量 */
  let e1 = c1.length - 1 /* 老vnode 最后一个节点的索引 */
  let e2 = l2 - 1    /* 新节点最后一个节点的索引 */

①第一步从头开始向尾寻找

(a b) c

(a b) d e

 /* 从头对比找到有相同的节点 patch ,发现不同,立即跳出*/
  while (i <= e1 && i <= e2) {
   const n1 = c1[i]
   const n2 = (c2[i] = optimized
    ? cloneIfMounted(c2[i] as VNode)
    : normalizeVNode(c2[i]))
    /* 判断key ,type是否相等 */
   if (isSameVNodeType(n1, n2)) {
    patch(
     n1,
     n2,
     container, 
     parentAnchor,
     parentComponent,
     parentSuspense,
     isSVG,
     optimized
    )
   } else {
    break
   }
   i++
  }

第一步的事情就是从头开始寻找相同的vnode,然后进行patch,如果发现不是相同的节点,那么立即跳出循环。

具体流程如图所示

③④主要针对新增和删除元素的情况,前提是元素没有发生移动, 如果有元素发生移动就要走⑤逻辑。 ③ 如果老节点是否全部patch,新节点没有被patch完,创建新的vnode

(a b)

(a b) c

i = 2, e1 = 1, e2 = 2

(a b)

c (a b)

i = 0, e1 = -1, e2 = 0

/* 如果新的节点大于老的节点数 ,对于剩下的节点全部以新的vnode处理( 这种情况说明已经patch完相同的vnode ) */
  if (i > e1) {
   if (i <= e2) {
    const nextPos = e2 + 1
    const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
    while (i <= e2) {
     patch( /* 创建新的节点*/
      null,
      (c2[i] = optimized
       ? cloneIfMounted(c2[i] as VNode)
       : normalizeVNode(c2[i])),
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG
     )
     i++
    }
   }
  }

i > e1

如果新的节点大于老的节点数 ,对于剩下的节点全部以新的vnode处理( 这种情况说明已经patch完相同的vnode ),也就是要全部create新的vnode.

具体逻辑如图所示

⑤ 不确定的元素 ( 这种情况说明没有patch完相同的vnode ),我们可以接着①②的逻辑继续往下看 diff核心

在①②情况下没有遍历完的节点如下图所示。

 const s1 = i //第一步遍历到的index
   const s2 = i 
   const keyToNewIndexMap: Map<string | number, number> = new Map()
   /* 把没有比较过的新的vnode节点,通过map保存 */
   for (i = s2; i <= e2; i++) {
    if (nextChild.key != null) {
     keyToNewIndexMap.set(nextChild.key, i)
    }
   }
   let j
   let patched = 0 
   const toBePatched = e2 - s2 + 1 /* 没有经过 path 新的节点的数量 */
   let moved = false /* 证明是否 */
   let maxNewIndexSoFar = 0 
   const newIndexToOldIndexMap = new Array(toBePatched)
    for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
   /* 建立一个数组,每个子元素都是0 [ 0, 0, 0, 0, 0, 0, ] */ 

遍历所有新节点把索引和对应的key,存入map keyToNewIndexMap中

keyToNewIndexMap存放 key -> index 的map

D : 2

E : 3

C : 4

I : 5

接下来声明一个新的指针 j ,记录剩下新的节点的索引。

patched,记录在第⑤步patched新节点过的数量

toBePatched记录⑤步之前,没有经过patched 新的节点的数量。

moved代表是否发生过移动,咱们的demo是已经发生过移动的。

newIndexToOldIndexMap用来存放新节点索引和老节点索引的数组。

newIndexToOldIndexMap 数组的index是新vnode的索引 , value是老vnode的索引。

接下来

 for (i = s1; i <= e1; i++) { /* 开始遍历老节点 */
    const prevChild = c1[i]
    if (patched >= toBePatched) { /* 已经patch数量大于等于, */
     /* ① 如果 toBePatched新的节点数量为0 ,那么统一卸载老的节点 */
     unmount(prevChild, parentComponent, parentSuspense, true)
     continue
    }
    let newIndex
     /* ② 如果,老节点的key存在 ,通过key找到对应的index */
    if (prevChild.key != null) {
     newIndex = keyToNewIndexMap.get(prevChild.key)
    } else { /* ③ 如果,老节点的key不存在 */
     for (j = s2; j <= e2; j++) { /* 遍历剩下的所有新节点 */
      if (
       newIndexToOldIndexMap[j - s2] === 0 && /* newIndexToOldIndexMap[j - s2] === 0 新节点没有被patch */
       isSameVNodeType(prevChild, c2[j] as VNode)
      ) { /* 如果找到与当前老节点对应的新节点那么 ,将新节点的索引,赋值给newIndex */
       newIndex = j
       break
      }
     }
    }
    if (newIndex === undefined) { /* ①没有找到与老节点对应的新节点,删除当前节点,卸载所有的节点 */
     unmount(prevChild, parentComponent, parentSuspense, true)
    } else {
     /* ②把老节点的索引,记录在存放新节点的数组中, */
     newIndexToOldIndexMap[newIndex - s2] = i + 1
     if (newIndex >= maxNewIndexSoFar) {
      maxNewIndexSoFar = newIndex
     } else {
      /* 证明有节点已经移动了  */
      moved = true
     }
     /* 找到新的节点进行patch节点 */
     patch(
      prevChild,
      c2[newIndex] as VNode,
      container,
      null,
      parentComponent,
      parentSuspense,
      isSVG,
      optimized
     )
     patched++
    }
 }

这段代码算是diff算法的核心。

第一步: 通过老节点的key找到对应新节点的index:开始遍历老的节点,判断有没有key, 如果存在key通过新节点的keyToNewIndexMap找到与新节点index,如果不存在key那么会遍历剩下来的新节点试图找到对应index。 第二步:如果存在index证明有对应的老节点,那么直接复用老节点进行patch,没有找到与老节点对应的新节点,删除当前老节点。 第三步:newIndexToOldIndexMap找到对应新老节点关系。

到这里,我们patch了一遍,把所有的老vnode都patch了一遍。

如图所示

如果所示当我们用index作为key的时候,无论我们怎么样移动删除节点,到了diff算法中都会从头到尾依次patch(图中: 所有节点均未有效的复用 )

②错误用法2 :用index拼接其他值作为key。

当已用index拼接其他值作为索引的时候,因为每一个节点都找不到对应的key,导致所有的节点都不能复用,所有的新vnode都需要重新创建。都需要重新create

如图所示。

四 总结

我们在上面,已经把刚开始的问题统统解决了,最后用一张思维脑图来从新整理一下整个流程。

到此这篇关于详解vue3.0 diff算法的使用(超详细)的文章就介绍到这了,更多相关vue3.0 diff算法内容请搜索来客网以前的文章或继续浏览下面的相关文章希望大家以后多多支持来客网!