tangyuxian
文章47
标签21
分类4
vue-diff算法

vue-diff算法

diff算法的本质是找出两个对象之间的差异,目的是尽可能复用节点。

引一 virtual dom

  1. template
  2. 渲染函数
  3. vnode(virtual dom)
  4. patch(diff算法)
  5. view

3a0a37c86e541c31d2c3ed3884ae6308.png

  • Vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树
  • VNode 虚拟节点:它可以代表一个真实的 dom 节点。通过 createElement 方法能将 VNode 渲染成 dom 节点。简单地说,vnode可以理解成节点描述对象,它描述了应该怎样去创建真实的DOM节点。
  • patch(也叫做patching算法):虚拟DOM最核心的部分,它可以将vnode渲染成真实的DOM,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新。这点我们从单词含义就可以看出, patch本身就有补丁、修补的意思,其实际作用是在现有DOM上进行修改来实现更新视图的目的。Vue的Virtual DOM Patching算法是基于Snabbdom的实现,并在些基础上作了很多的调整和改进。

什么是virtual dom

通俗易懂的来说就是用一个简单的对象去代替复杂的dom对象。

如果你去打印一下一个真实的DOM节点,就会发现DOM节点上有很多属性,如果Vue每次都生成一个新的真实DOM节点,对性能是巨大的浪费。

Virtual DOM 实际上就是以JavaScript对象(VNode )为基础形成一棵树,对真实DOM的一层抽象。Vue最终的工作就是通过这棵树批量生成真实的DOM节,可以说两者存在一层映射关系。

简单来说,可以把Virtual DOM 理解为一个简单的JS对象,并且最少包含标签名( tag)、属性(attrs)和子元素对象( children)三个属性。不同的框架对这三个属性的命名会有点差别。

对于虚拟DOM,咱们来看一个简单的实例,就是下图所示的这个,详细的阐述了模板 → 渲染函数 → 虚拟DOM树 → 真实DOM的一个过程

dbc527b02888fdd4be3860945e85fa02.png

其实虚拟DOM在Vue.js主要做了两件事:

  • 提供与真实DOM节点所对应的虚拟节点vnode
  • 将虚拟节点vnode和旧虚拟节点oldVnode进行对比(diff算法),然后更新视图

总结:「vdom是为了减轻性能压力。dom是昂贵的,昂贵的一方面在dom本身的重量,dom节点在js里的描述是个非常复杂属性很多原型很长的超级对象,另一方面是浏览器渲染进程和js进程是独立分离的,操作dom时的通信和浏览器本身需要重绘的时耗都是很高的。所以大家机智的搞了个轻量的vdom去模拟dom,vdom每个节点都只挂载js操作的必要属性,每次组件update时都先操作vdom,通过vdom的比对,得到一个真实dom的需要操作的集合。整个机制是在JavaScript层面计算,在完成之前并不会操作DOM,等数据稳定之后再实现精准的修改。」

引二 分析diff算法

由上我们知道了,新的虚拟DOM和旧的虚拟DOm是通过diff算法进行比较之后更新的。

Vue2.x diff算法

Vue2.x diff算法原理

传统diff算法通过循环递归对节点进行依次对比效率低下,算法复杂度达到O(N^3),主要原因在于其追求完全比对和最小修改,而React、Vue则是放弃了完全比对及最小修改,才实现从O(N^3) => O(N)。

优化措施有:

  • 「分层diff」:不考虑跨层级移动节点,让新旧两个VDOM树的比对无需循环递归(复杂度大幅优化,直接下降一个数量级的首要条件)。这个前提也是Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。

在同层节点中,采用了「双端比较的算法」过程可以概括为:oldCh和newCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一个已经遍历完了,就会结束比较;

13ce1945ff1e74d43467f5c3ef82b16f.png

当发生以下情况则跳过比对,变为插入或删除操作:

  • 「组件的Type(Tagname)不一致」,原因是绝大多数情况拥有相同type的两个组件将会生成相似的树形结构,拥有不同type的两个组件将会生成不同的树形结构,所以type不一致可以放弃继续比对。
  • 「列表组件的Key不一致」,旧树中无新Key或反之。毕竟key是元素的身份id,能直接对应上是否是同一个节点。
  • 对触发了getter/setter 的组件进行diff,精准减少diff范围

Vue3.0 diff

diff痛点

vue2.x中的虚拟dom是进行「全量的对比」,在运行时会对所有节点生成一个虚拟节点树,当页面数据发生变更好,会遍历判断virtual dom所有节点(包括一些不会变化的节点)有没有发生变化;虽然说diff算法确实减少了多DOM节点的直接操作,但是这个「减少是有成本的」,如果是复杂的大型项目,必然存在很复杂的父子关系的VNode,「而Vue2.x的diff算法,会不断地递归调用 patchVNode,不断堆叠而成的几毫秒,最终就会造成 VNode 更新缓慢」

那么Vue3.0是如何解决这些问题的呢

动静结合 PatchFlag

在Vue3.0中,在这个模版编译时,编译器会在动态标签末尾加上 /* Text*/ PatchFlag。「也就是在生成VNode的时候,同时打上标记,在这个基础上再进行核心的diff算法」并且 PatchFlag 会标识动态的属性类型有哪些,比如这里 的TEXT 表示只有节点中的文字是动态的。而patchFlag的类型也很多。这里直接引用一张图片。

21d07297b28cf0c636daa79431065f2a.png

其中大致可以分为两类:

  • 当 patchFlag 的值「大于」 0 时,代表所对应的元素在 patchVNode 时或 render 时是可以被优化生成或更新的。
  • 当 patchFlag 的值「小于」 0 时,代表所对应的元素在 patchVNode 时,是需要被 full diff,即进行递归遍历 VNode tree 的比较更新过程。

看源码:

export function render(_ctx, _cache, $props, $setup, $data, $options) {
 return (_openBlock(), _createBlock("div", null, [
  _createVNode("p", null, "'HelloWorld'"),
  _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
 
 ]))
}
****

这里的_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)就是对变量节点进行标记。

总结:「Vue3.0对于不参与更新的元素,做静态标记并提示,只会被创建一次,在渲染时直接复用。」

可参考CSDN文章:Vue3的diff算法优化部分


一 什么时候用到了diff算法,diff算法作用域?

1.1diff算法的作用域

patch概念引入

在vue update过程中在遍历子代vnode的过程中,会用不同的patch方法来patch新老vnode,如果找到对应的 newVnode 和 oldVnode,就可以复用利用里面的真实dom节点。避免了重复创建元素带来的性能开销。毕竟浏览器创造真实的dom,操纵真实的dom,性能代价是昂贵的。

patch过程中,如果面对当前vnode存在有很多chidren的情况,那么需要分别遍历patch新的children Vnode和老的 children vnode。

存在chidren的vnode类型

首先思考一下什么类型的vnode会存在children。

①element元素类型vnode

第一中情况就是element类型vnode 会存在 children vode,此时的三个span标签就是chidren vnode情况

<div>
   <span> 苹果🍎 </span> 
   <span> 香蕉🍌 </span>
   <span> 鸭梨🍐 </span>
</div>

在vue3.0源码中 ,patchElement用于处理element类型的vnode

②flagment碎片类型vnode

在Vue3.0中,引入了一个fragment碎片概念。
你可能会问,什么是碎片?如果你创建一个Vue组件,那么它只能有一个根节点。

<template>
   <span> 苹果🍎 </span> 
   <span> 香蕉🍌 </span>
   <span> 鸭梨🍐 </span>
</template>

这样可能会报出警告,原因是代表任何Vue组件的Vue实例需要绑定到一个单一的DOM元素中。唯一可以创建一个具有多个DOM节点的组件的方法就是创建一个没有底层Vue实例的功能组件。

flagment出现就是用看起来像一个普通的DOM元素,但它是虚拟的,根本不会在DOM树中呈现。这样我们可以将组件功能绑定到一个单一的元素中,而不需要创建一个多余的DOM节点。

 <Fragment>
   <span> 苹果🍎 </span> 
   <span> 香蕉🍌 </span>
   <span> 鸭梨🍐 </span>
</Fragment>

在vue3.0源码中 ,processFragment用于处理Fragment类型的vnode

1.2 patchChildren

从上文中我们得知了存在children的vnode类型,那么存在children就需要patch每一个
children vnode依次向下遍历。那么就需要一个patchChildren方法,依次patch子类vnode。

patchChildren

vue3.0中 在patchChildren方法中有这么一段源码

if (patchFlag > 0) {
      if (patchFlag & PatchFlags.KEYED_FRAGMENT) { 
         /* 对于存在key的情况用于diff算法 */
        patchKeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
        return
      } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
         /* 对于不存在key的情况,直接patch  */
        patchUnkeyedChildren( 
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
        return
      }
    }

patchChildren根据是否存在key进行真正的diff或者直接patch。

既然diff算法存在patchChildren方法中,而patchChildren方法用在Fragment类型和element类型的vnode中,这样也就解释了diff算法的作用域是什么。

1.3 diff算法作用?

通过前言我们知道,存在这children的情况的vnode,需要通过patchChildren遍历children依次进行patch操作,如果在patch期间,再发现存在vnode情况,那么会递归的方式依次向下patch,那么找到与新的vnode对应的vnode显的如此重要。

我们用两幅图来向大家展示vnode变化。

WechatIMG13940.jpeg
WechatIMG13941.jpeg

如上两幅图表示在一次更新中新老dom树变化情况。

假设不存在diff算法,依次按照先后顺序patch会发生什么

如果不存在diff算法,而是直接patchchildren 就会出现如下图的逻辑。

WechatIMG13942.jpeg

第一次patchChidren

WechatIMG13943.jpeg

第二次patchChidren

WechatIMG13944.jpeg

第三次patchChidren

WechatIMG13945.jpeg

第四次patchChidren

WechatIMG13947.jpeg
如果没有用到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,如果发现不是相同的节点,那么立即跳出循环。

具体流程如图所示

在这里插入图片描述

isSameVNodeType

export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  return n1.type === n2.type && n1.key === n2.key
}

isSameVNodeType 作用就是判断当前vnode类型 和 vnode的 key是否相等

②第二步从尾开始同前diff

a (b c)
d e (b c)

 /* 如果第一步没有patch完,立即,从后往前开始patch ,如果发现不同立即跳出循环 */
    while (i <= e1 && i <= e2) {
      const n1 = c1[e1]
      const n2 = (c2[e2] = optimized
        ? cloneIfMounted(c2[e2] as VNode)
        : normalizeVNode(c2[e2]))
      if (isSameVNodeType(n1, n2)) {
        patch(
          n1,
          n2,
          container,
          parentAnchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      } else {
        break
      }
      e1--
      e2--
    }

经历第一步操作之后,如果发现没有patch完,那么立即进行第二部,从尾部开始遍历依次向前diff。

如果发现不是相同的节点,那么立即跳出循环。

具体流程如图所示
WechatIMG13952.jpeg

③④主要针对新增和删除元素的情况,前提是元素没有发生移动, 如果有元素发生移动就要走⑤逻辑。

③ 如果老节点是否全部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.

具体逻辑如图所示
WechatIMG13955.jpeg

④ 如果新节点全部被patch,老节点有剩余,那么卸载所有老节点

i > e2
(a b) c
(a b)
i = 2, e1 = 2, e2 = 1
a (b c)
(b c)
i = 0, e1 = 0, e2 = -1

else if (i > e2) {
   while (i <= e1) {
      unmount(c1[i], parentComponent, parentSuspense, true)
      i++
   }
}

对于老的节点大于新的节点的情况 ,对于超出的节点全部卸载 ( 这种情况说明已经patch完相同的vnode )

具体逻辑如图所示
WechatIMG13956.jpeg

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

diff核心

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

剩下的节点。

WechatIMG13953.jpeg

      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了一遍。

如图所示
01B449A5-9B15-4547-8EBF-5A3BA66A1831.jpg
但是接下来的问题。

1 虽然已经patch过所有的老节点。可以对于已经发生移动的节点,要怎么真正移动dom元素。
2 对于新增的节点,(图中节点I)并没有处理,应该怎么处理。

      /*移动老节点创建新节点*/
     /* 根据最长稳定序列移动相对应的节点 */
      const increasingNewIndexSequence = moved
        ? getSequence(newIndexToOldIndexMap)
        : EMPTY_ARR
      j = increasingNewIndexSequence.length - 1
      for (i = toBePatched - 1; i >= 0; i--) {
        const nextIndex = s2 + i
        const nextChild = c2[nextIndex] as VNode
        const anchor =
          nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
        if (newIndexToOldIndexMap[i] === 0) { /* 没有老的节点与新的节点对应,则创建一个新的vnode */
          patch(
            null,
            nextChild,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG
          )
        } else if (moved) {
          if (j < 0 || i !== increasingNewIndexSequence[j]) { /*如果没有在长*/
            /* 需要移动的vnode */
            move(nextChild, container, anchor, MoveType.REORDER)
          } else {
            j--
          }    

⑥最长稳定序列

首选通过getSequence得到一个最长稳定序列,对于index === 0 的情况也就是新增节点(图中I) 需要从新mount一个新的vnode,然后对于发生移动的节点进行统一的移动操作

什么叫做最长稳定序列

对于以下的原始序列
0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15
最长递增子序列为
0, 2, 6, 9, 11, 15.

为什么要得到最长稳定序列

因为我们需要一个序列作为基础的参照序列,其他未在稳定序列的节点,进行移动。

总结

经过上述我们大致知道了diff算法的流程
1 从头对比找到有相同的节点 patch ,发现不同,立即跳出。

2如果第一步没有patch完,立即,从后往前开始patch ,如果发现不同立即跳出循环。

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

4 对于老的节点大于新的节点的情况 , 对于超出的节点全部卸载 ( 这种情况说明已经patch完相同的vnode )。

5不确定的元素( 这种情况说明没有patch完相同的vnode ) 与 3 ,4对立关系。

1 把没有比较过的新的vnode节点,通过map保存
记录已经patch的新节点的数量 patched
没有经过 path 新的节点的数量 toBePatched
建立一个数组newIndexToOldIndexMap,每个子元素都是[ 0, 0, 0, 0, 0, 0, ] 里面的数字记录老节点的索引 ,数组索引就是新节点的索引
开始遍历老节点
① 如果 toBePatched新的节点数量为0 ,那么统一卸载老的节点
② 如果,老节点的key存在 ,通过key找到对应的index
③ 如果,老节点的key不存在

  1 遍历剩下的所有新节点
  2 如果找到与当前老节点对应的新节点那么 ,将新节点的索引,赋值给newIndex

④ 没有找到与老节点对应的新节点,卸载当前老节点。
⑤ 如果找到与老节点对应的新节点,把老节点的索引,记录在存放新节点的数组中,

  1 如果节点发生移动 记录已经移动了
  2 patch新老节点 找到新的节点进行patch节点    

遍历结束

如果发生移动

① 根据 newIndexToOldIndexMap 新老节点索引列表找到最长稳定序列
② 对于 newIndexToOldIndexMap -item =0 证明不存在老节点 ,从新形成新的vnode 
③ 对于发生移动的节点进行移动处理。 

三 key的作用,如何正确key。

1key的作用

在我们上述diff算法中,通过isSameVNodeType方法判断,来判断key是否相等判断新老节点。
那么由此我们可以总结出?

在v-for循环中,key的作用是:通过判断newVnode和OldVnode的key是否相等,从而复用与新节点对应的老节点,节约性能的开销。

2如何正确使用key

①错误用法 1:用index做key。

用index做key的效果实际和没有用diff算法是一样的,为什么这么说呢,下面我就用一幅图来说明:

WechatIMG10408 1.jpeg

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

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

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

如图所示。
WechatIMG10531.jpeg

③正确用法 :用唯一值id做key(我们可以用前后端交互的数据源的id为key)。

如图所示。每一个节点都做到了复用。起到了diff算法的真正作用。

WechatIMG10532.jpeg

四 总结

我们在上面,已经把刚开始的问题统统解决了,最后用一张思维脑图来从新整理一下整个流程。
7FB635D9-EE62-45D5-8DE9-620CDF74B772.jpg


参考文章:

CSDN文章:diff算法_Vue3.0时代你必须了解的:diff算法原理和优化

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

本文作者:tangyuxian
本文链接:https://www.tangyuxian.com/2021/07/14/%E5%89%8D%E7%AB%AF/vue/vue-diff%E7%AE%97%E6%B3%95/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可