【VUE】— diff算法原理

系列文章目录

【VUE】— watch侦听器原理

一、简介

vue中会维护一个和 DOM 节点对应的 vnode 对象。

vnode children 数组中对应子节点的 vnode 对象,所以在 vue 中通过 vnode 和真实的 DOM 树进行映射,我们也称之为虚拟树。

正是有了虚拟树,当数据更新时。我们可以对比新数据构建的 vnode 和老数据构建的 oldVnode 的差异,来实现精准更新。

而我们对比差异的算法就是采用的 diff。通过 diff 对比虚拟树的差异,将差异通过打补丁(patch)的方式更新到对应的真实 DOM 节点上。

二、源码分析

2.1 patch函数

patch函数是 diff 流程的入口函数。首先我们要知道,patch会先在页面渲染的时候加载一次,然后就在vnode改变的时候调用。

// 首次渲染是DOM元素,后面是vnode
function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    
    
    let i: number, elm: Node, parent: Node
    const insertedVnodeQueue: VNodeQueue = []
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()

	// 不是一个vnode就创建一个空的vnode并关联在DOM元素上
    if (!isVnode(oldVnode)) {
    
    
      // 创建一个空的vnode,并关联DOM元素
      oldVnode = emptyNodeAt(oldVnode)
    }

    if (sameVnode(oldVnode, vnode)) {
    
    
      // key、tag相同,说明是同一个vnode
      patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
    
    
      // key、tag不相同,说明是不同的vnode
      elm = oldVnode.elm!
      parent = api.parentNode(elm) as Node
      
      // 创建新的DOM元素
      createElm(vnode, insertedVnodeQueue)

      if (parent !== null) {
    
    
        // 插入新的DOM元素
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
        // 移除老的DOM元素
        removeVnodes(parent, [oldVnode], 0, 0)
      }
    }
    return vnode
  }

整个逻辑就是:
如果两个节点都是一样的,那么就深入检查他们的子节点。如果两个节点不一样那就说明vnode完全被改变了,就可以直接替换oldVnode

2.2 sameVnode

// 判断新旧节点是否一样
function sameVnode (a, b) {
    
    
  return (
    a.key === b.key &&  // key值
    a.tag === b.tag &&  // 标签名
    a.isComment === b.isComment &&  // 是否为注释节点
    // 是否都定义了data,data包含一些具体信息,例如onclick , style
    isDef(a.data) === isDef(b.data) &&  
    sameInputType(a, b) // 当标签是<input>的时候,type必须相同
  )
}

2.3 patchVnode

如果两个节点是一样的,就会进入patchVnode的方法,判断他的子节点和文本节点。这里也就是对可复用节点进行打补丁,也就是派发更新。

  function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    
    
   	...
    // vonde为新的vnode oldvnnode为老的vnode
    // 设置vnode关联的DOM元素 
    const elm = vnode.elm = oldVnode.elm!

    // 老children
    const oldCh = oldVnode.children as VNode[]
    
    // 新children
    const ch = vnode.children as VNode[]

    if (oldVnode === vnode) return
	...
    // 新vnode 无text 有children
    if (isUndef(vnode.text)) {
    
    
      // 新vnode 有children 老vnode 有chidren
      if (isDef(oldCh) && isDef(ch)) {
    
    
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
      // 新vnode 有children 旧vnode 无children 有text
      } else if (isDef(ch)) {
    
    
        // 清空text
        if (isDef(oldVnode.text)) api.setTextContent(elm, '')
        // 添加children
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        
      // 新vnode 无children 旧vnode 有children
      } else if (isDef(oldCh)) {
    
    
        // 移除children
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        
      // 老vnode 有text
      } else if (isDef(oldVnode.text)) {
    
    
        // 清空text
        api.setTextContent(elm, '')
      }
    // 新vnode 有text 无children
    // 老vnode text 不等于 新vnode text
    } else if (oldVnode.text !== vnode.text) {
    
    
      // 老vnode 有children
      if (isDef(oldCh)) {
    
    
        // 移除老vnode children
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      }
      // 设置新vnode text
      api.setTextContent(elm, vnode.text!)
    }
    hook?.postpatch?.(oldVnode, vnode)
  }

patchVnode函数的逻辑:

  1. 找到对应的DOM节点 elm,并且赋值给新的vnode.elm
  2. 判断新节点类型(vnode.text),如果是文本节点,更新 elm 文本即可
  3. 非文本节点下,判断新老节点的子节点
  4. 如果新老节点都有子节点,走子节点的同层比较流程 updateChildren
  5. 如果只有新节点有子节点,直接使用 addVnodeselm 添加子节点(先删除文本)
  6. 如果只有旧节点有子节点,使用 removeVnodes 移除即可
  7. 如果都没有子节点,判断旧数据是否有文本节点,有则清空。

2.4 updateChildren

 function updateChildren (parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue) {
    
    
    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: KeyToIndexMap | undefined
    let idxInOld: number
    let elmToMove: VNode
    let before: any
​
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    
    
      if (oldStartVnode == null) {
    
    
        oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
      } else if (oldEndVnode == null) {
    
    
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (newStartVnode == null) {
    
    
        newStartVnode = newCh[++newStartIdx]
      } else if (newEndVnode == null) {
    
    
        newEndVnode = newCh[--newEndIdx]// 1.老开始和新开始对比
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
    
    
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]// 2.老结束和新结束对比
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
    
    
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]// 3.老开始和新结束对比
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
    
     // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]// 4.老结束和新开始对比
      } else if (sameVnode(oldEndVnode, newStartVnode)) {
    
     // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]// 以上四种情况都没有命中
      } else {
    
    
        if (oldKeyToIdx === undefined) {
    
    
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        }// 拿到新开始的key,在老children里去找有没有某个节点有对应这个key
        idxInOld = oldKeyToIdx[newStartVnode.key as string]
        // 没有在老children里找到对应的
        if (isUndef(idxInOld)) {
    
     // New element
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)// 在老children里找到了对应的
        } else {
    
    
          // 找到了对应key的元素(key相等)
          elmToMove = oldCh[idxInOld]// key相等 判断tag是否相等
          if (elmToMove.sel !== newStartVnode.sel) {
    
    
            // key相等 tag不相等
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
          } else {
    
    
            // key相等 tag相等
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined as any
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    //当节点遍历完之后,会存在两种情况,“新数组已经遍历完,但旧数组没有遍历完成” 和 “旧数组遍历完成,但新数组没有遍历完成”。故源代码的判断如下:
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
    
    
      if (oldStartIdx > oldEndIdx) {
    
    
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
      } else {
    
    
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
      }
    }
  }

分析:
在这里插入图片描述

  • 第一步:判断oldStartVnode, newStartVnode他俩指针指的vnode是否匹配上了,如果匹配上了,oldStartVnode,newStartVnode指针向移动,真实DOM的位置不变。
  • 第二步:oldEndVnode, newEndVnode他俩是否匹配上了,如果匹配上了,oldEndVnode, newEndVnode指针向移动,真实DOM的位置不变。
  • 第三步:oldStartVnode, newEndVnode判断是否匹配上了,如果匹配上了那么真实DOM中的第一个节点会移到最后
  • 第四步:oldEndVnode, newStartVnode判断匹配上了,如果匹配上了那么真实DOM中的最后一个节点会移到最前,匹配上的两个指针向中间移动。
  • 第五步:如果四种匹配没有一对是成功的,分为两种情况
    • 如果新旧子节点都存在key,那么会根据oldChildkey生成一张hash表,用newStartVnodekeyhash表做匹配,匹配成功就判断newStartVnode和匹配节点是否为sameNode,如果是,就在真实DOM中将成功的节点移到最前面,否则,将newStartVnode生成对应的节点插入到DOM中对应的newStartVnode位置,newStartVnode指针向中间移动,被匹配old中的节点置为null
    • 如果没有key,则直接将newStartVnode生成新的节点插入真实DOM(ps:这下可以解释为什么v-for的时候需要设置key了,如果没有key那么就只会做四种匹配,就算指针中间有可复用的节点都不能被复用了)

三、vue3中对于diff算法的优化

vue3.0针对“无脑”patchVnode进行了过滤–静态类型Vnode
老版的源码:
在这里插入图片描述
这里,我们再重复下vue2.x系列的对比更新逻辑:
在这里插入图片描述
新版的vue3增加了静态类型Vnode,如果是静态类型的vnode 那么直接跳过更新,修改新节点引用即可:
在这里插入图片描述
备注:comment类型目前翻到它的源码也只是更改引用,源码作者加上了一行注释。
在这里插入图片描述
这里再加一句,flagment碎片类型为新增的vnode类型, 即:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_44761091/article/details/124141686