diff算法

虚拟DOM和真实DOM

真实DOM是以各种网页元素为节点组成的一颗树,而虚拟DOM就是模拟树形结构的数组。虚拟DOM相对于真实DOM操作简单、快捷,所以在构造真实DOM前,会将需要的操作在虚拟DOM上完成,再使用虚拟DOM构造真实DOM。

比如以下网页代码

<ul id="list">
    <li class="item">哈哈</li>
    <li class="item">呵呵</li>
    <li class="item">嘿嘿</li>
</ul>

对应的真实DOM为(节点上的属性如id、class等省略)
在这里插入图片描述
对应的虚拟DOM为:

let VDOM = { 
  tagName: 'ul', // 标签名
  props: { // 标签属性
      id: 'list'
  },
  children: [ // 标签子节点
      {
          tagName: 'li', props: { class: 'item' }, children: ['哈哈']
      },
      {
          tagName: 'li', props: { class: 'item' }, children: ['呵呵']
      },
      {
          tagName: 'li', props: { class: 'item' }, children: ['嘿嘿']
      },
  ]
}

这时候,如果修改修改一个li标签的文本:

<ul id="list">
    <li class="item">哈哈</li>
    <li class="item">呵呵</li>
    <li class="item">林三心哈哈哈哈哈</li> // 修改
</ul>

真实DOM的对应节点的文本内容会改变,生成的新虚拟DOM为:

let newVDOM = { // 新虚拟DOM
  tagName: 'ul', // 标签名
  props: { // 标签属性
      id: 'list'
  },
  children: [ // 标签子节点
      {
          tagName: 'li', props: { class: 'item' }, children: ['哈哈']
      },
      {
          tagName: 'li', props: { class: 'item' }, children: ['呵呵']
      },
      {
          tagName: 'li', props: { class: 'item' }, children: ['林三心哈哈哈哈哈']
      },
  ]
}

虚拟DOM为什么可以提高性能

1、虚拟DOM是对真实DOM的抽象,去除了很多DOM节点的属性,提高了比较的效率
2、diff算法需要虚拟DOM的配合,每个操作都去更新虚拟DOM而不是把每个操作应用到真实DOM上触发多次回流和重绘

Vue中的diff算法

背景

如果每次都用新的虚拟DOM去更新真实DOM时,那么整颗树都要重新渲染,所以产生了diff算法,需要新的虚拟DOM和旧的虚拟DOM,找到新旧虚拟DOM中不同的位置,只对不同位置进行真实DOM的更新。

比较策略

先贴一下源码

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

    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) 
        //没有相同的key,没有对应的旧节点,为新节点
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {	//有相同的key
          vnodeToMove = oldCh[idxInOld]	//找到该key对应的旧节点
          //再一次判断相同key的新旧节点是否为完全相同
          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(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

diff算法只在DOM树的同层级进行比较,不会跨层比较,所以可以把同层的数据看成处于一个数组中,新虚拟DOM为newCh,旧虚拟DOM为oldCh,对应源码。

该函数定义了4个指针,分别指向新旧DOM的首尾。

可以看到循环中分7种情况:
前两种情况是对未定义节点的处理,此处忽略。
后5种情况:
1、旧虚拟DOM首节点和新虚拟DOM首节点使用sameVnode方法(判断节点类型是否相同)进行比较,如果判断是相同节点,那么直接复用,更新到新节点相应位置,原位置节点置为undefined。
2、旧虚拟DOM尾节点和新虚拟DOM尾节点使用sameVnode方法进行比较,处理与1相同
3、旧虚拟DOM首节点和新虚拟DOM尾节点使用sameVnode方法进行比较,处理与1相同
4、旧虚拟DOM尾节点和新虚拟DOM首节点使用sameVnode方法进行比较,处理与1相同
5、如果以上逻辑都匹配不到,再把所有旧子节点的 key 做一个映射到旧节点下标的 key -> index 表,然后遍历该表找出该新节点在旧节点中有无可复用的节点。(遍历没有被判断过的旧节点,首先找到key相同的节点,若没有,则直接创建新节点;若有,使用sameNode方法判断两个节点是否相同,如果是,则复用,再把该节点移动到新节点的位置,原位置节点置为undefined;如果不是,就当作新节点,创建节点,插入到指定位置)

如果有一个数组已经遍历完就跳出循环,此时已经完成较短数组的所有处理,剩下未处理的是较长数组中的节点。如果是旧数组较长,那么需要删除剩余节点;如果是新数组较长,那么需要新增剩余节点。

key

说到key,就要讲一下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出现在a.key === b.key中,并且位于判断条件的第一项。这就意味着,我们为节点设置key的目的是标记那些完全一样的节点,帮助diff操作更加准确和快速。

无key

如果没有key,那么a.key和b.key都为undefined,则条件成立。并且由于后面的判断条件较松,所以很多不应该被拿来复用的节点被判断成sameVnode进行patch,这就导致了就地复用(直接将新老虚拟DOM中对应位置的节点进行复用),需要进行大量的操作来使两个节点相同,并且原本可以复用的节点也因为此原因需要拆掉重建,从而消耗性能。

有key

如果有key,那么会根据唯一的key来判断节点是否为相同的,当两个节点key相同时,意味着新老节点没有差异,可以完全复用,此时的操作就比较简单,并且会将老虚拟DOM中所有可复用的节点全部复用,从而提升性能。

其实有key也可以用key来生成map,直接用key来查找,比遍历更加快速

old:a b c d
new: b c d a
有key情况和无key的情况

根据diff算法的比较策略
无key:从前往后每个节点都会被判断成相同,所以从前往后依次patch内容

有key:根据key来找可复用的节点,所以顺序是
先把a移动到最后
从b开始依次从前往后复用

react和vue的diff区别

  1. vue比对节点,当节点元素类型相同,但是className不同,认为是不同类型元素,删除重建,而react会认为是同类型节点,只是修改节点属性

  2. vue的列表比对,采用从两端到中间的比对方式,而react则采用从左到右依次比对的方式。当一个集合,只是把最后一个节点移动到了第一个,react会把前面的节点依次移动,而vue只会把最后一个节点移动到第一个。总体上,vue的对比方式更高效。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值