目录
- 前言
 - 一、认识虚拟DOM
 - 用 JS 对象模拟 DOM 结构
 - 用JS对象模拟DOM节点的好处
 - 为什么要使用虚拟 DOM 呢?
 - 虚拟Dom 和 diff算法的关系
 
- 二、认识diff算法
 - diff算法的优化
 - key的作用
 - diff算法 在什么时候执行?
 
- 三、深入diff算法源码
 - patch 函数
 - sameVnode 函数
 - patchVnode 函数
 - updateChildren 函数
 - 为什么会有头对尾,尾对头的操作?
 
- 总结
 - 1. 虚拟DOM的解析过程
 - 2. diff 算法的原理
 
前言
这是一个系列学习源码的文章,感兴趣的可以继续阅读其他文章
 Vue源码学习 - new Vue初始化都做了什么?
 Vue源码学习 - 数据响应式原理
 Vue源码学习 - 异步更新队列 和 nextTick原理
因为 Diff 算法,计算的就是虚拟 DOM 的差异,所以先铺垫一点点虚拟 DOM,了解一下其结构,再去看Diff 算法原理,循循渐进会更好些。
渲染真实的 DOM 时,并不是暴力覆盖原有的 DOM ,而是比对新旧两个vnode(虚拟节点),如果不是同一个节点,删除老的,替换成新的;如果是同一个节点,就复用老节点,增加新节点的属性。
一、认识虚拟DOM
虚拟 DOM 简单说就是 用JS对象来模拟 DOM 结构。
用 JS 对象模拟 DOM 结构
用 JS 对象模拟 DOM 结构的例子:
<template>
    <div id="app" class="container">
        <h1>铁锤妹妹</h1>
    </div>
</template>
 
上面的模板转成 JS对象 就是下面这样。
 这样的 DOM 结构就称之为 虚拟 DOM (Virtual Node),简称 vnode。
{
  tag:'div',
  props:{ id:'app', class:'container' },
  children: [
    { tag: 'h1', children:'铁锤妹妹' }
  ]
}
 
它的表达方式就是把每一个标签都转为一个对象,这个对象可以有三个属性:tag、props、children。
- tag:必选。就是标签也可以是组件或者函数。
 - props:非必选。就是这个标签上的属性和方法。
 - children:非必选。就是这个标签的内容或者子节点,如果是文本节点就是字符串,如果有子节点就是数组。换句话说 如果判断 children 是字符串的话,就表示一定是文本节点,这个节点肯定没有子元素
 
用JS对象模拟DOM节点的好处
假设在一次操作中有1000个节点 需要更新,那么 虚拟DOM 不会立即去操作Dom,而将这1000次更新的 diff 内容保存到本地的一个JS对象上,之后将这个 JS 对象一次性 attach 到 DOM 树上,最后再进行后续的操作,这样子就避免了大量没必要的计算。
所以,用JS对象模拟DOM节点的好处就是:先将页面的更新全部反映到 虚拟DOM 上,这样子就 先操作内存中的JS对象。值得注意的是,操作内存中的 JS对象 速度是相当快的。然后等到全部DOM节点更新完成后,再将最后的 JS对象 映射到 真实DOM 上,交给 浏览器 去绘制。
这样就解决了 真实DOM 渲染速度慢,性能消耗大 的问题。
为什么要使用虚拟 DOM 呢?
我们先创建一个空div,打印看看上面自带的所有属性和事件。
    let div = document.createElement('div')
    let props = ''
    for (let key in div) {
      props += key + ' '
    }
    console.log(props)
 
打印结果:

如图可以看出原生 DOM 有非常多的属性和事件,就算是创建一个空div也要付出不小的代价。而使用虚拟 DOM 来提升性能的点在于 DOM 发生变化的时候,通过 diff 算法和数据改变前的 DOM 对比,计算出需要更改的 DOM,然后只对变化的 DOM 进行操作,而不是更新整个视图。
虚拟Dom 和 diff算法的关系
其实,vdom 是一个大的概念,而 diff算法 是 vdom 中的一部分。vdom 的核心价值在于最大程度上减少 真实DOM 的频繁更新。
 vdom 通过把 DOM 用 JS的方式 进行模拟,通过比较新旧虚拟DOM,只更新差异部分,然后批量操作真实DOM,减少了对真实DOM 的频繁操作,提高了性能。那么这个对比的过程就是diff算法。也就是说两者是包含关系,如下图所示:
 
二、认识diff算法
diff算法的优化
假如有1000个节点,就需要计算 1000³ 次,也就是10亿次,这样是无法接受的,所以 Vue 里使用 Diff 算法的时候都遵循深度优先,同层比较的策略做了一些优化,来计算出最小变化。
1)只比较同一层级,不跨级比较
Diff 过程只会把同颜色框起来的同一层级的 DOM 进行比较,这样来简化比较次数。
 
2) 比较tag标签名
 如果同一层级的标签名不同,就直接删掉旧的虚拟 DOM 重建,不继续做深度比较。

3)比较 key
 如果标签名相同,key 也相同,就会认为是相同节点,不继续做深度比较。
 比如我们写 v-for 的时候会比较 key,不写 key 就会报错,这也就是因为 Diff 算法需要比较 key。
key的作用
通过图形例子会更好理解些:
 比如有一个列表,需要在 列表中间 插入一个元素,会发生什么变化呢?先看个图
 
如图的 li1 和 li2 不会重新渲染,而 li3、li4、li5 都会重新渲染。
因为在不使用 key 或者列表的 index 作为 key 的时候,每个元素对应的位置关系都是索引 index,上图中的结果直接导致我们插入的元素到后面的全部元素,对应的位置关系都发生了变更,所以全部都会执行更新操作,这不是我们想要的,我们希望的是只渲染添加的那一个元素 li5,其他四个元素不做任何变更,就地复用就好,不要重新渲染。
而在使用唯一 key 的情况下,每个元素对应的位置关系就是 key,看一下使用唯一 key 值的情况下:

这样如图中的 li3 和 li4 就不会重新渲染,因为元素内容没发生改变,对应的位置关系也没有发生改变。
 这也是为什么 v-for 必须要写 key,而且不建议开发中使用数组的 index 作为 key 的原因。
总结一下:
- key 的作用主要是为了更高效的更新虚拟 DOM,因为它可以非常精确的找到相同节点,diff 操作可以更高效。
 
如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单“就地复用”此处的每个元素。
diff算法 在什么时候执行?
1. 页面
首次渲染的时候,会调用一次 patch 并创建新的 vnode,不会进行更深层次的比较。
2. 然后就是在组件中的数据发生变化的时候,会触发setter,然后通过notify()通知watcher,对应的watcher会通知更新,并执行更新函数,它会执行render函数获取新的虚拟 DOM,然后执行patch对比旧的虚拟 DOM,并计算出最小变化,然后再去根据这个最小变化去更新真实的DOM,也就是视图更新。
三、深入diff算法源码
patch 函数
用于比较新旧 VNode,并进行 DOM 更新的核心函数。
需要注意的是,patch 函数在进行 DOM 更新时会尽可能地复用已有的 DOM 元素和节点,从而提高性能。它会通过对比新旧 VNode 的差异,只对真正发生变化的部分进行更新,而不会重新创建整个 DOM 结构。
主要流程是这样的:
-  
vnode 不存在,oldVnode 存在,就删掉 oldVnode。(vnode 不存在表示组件被移除或不在需要渲染,为保持视图与数据的同步,所以删掉 oldVnode)
 -  
vnode 存在,oldVnode 不存在,就创建 vnode。
 -  
两个都存在的话,通过
sameVnode()比较两者是否是同一节点。1)如果是同一节点的话,通过
patchVnode()函数进行后续对比节点文本变化或者子节点变化。
2)如果不是同一节点,则删除该节点重新创建新节点进行替换(对于组件节点,Vue 将尽可能地复用已有的组件实例,而不是销毁和重新创建组件) 
// src/core/vdom/patch.ts
// 两个判断函数
export function isUndef(v: any): v is undefined | null {
  return v === undefined || v === null
}
export function isDef<T>(v: T): v is NonNullable<T> {
  return v !== undefined && v !== null
}
 return function patch(oldVnode, vnode, hydrating, removeOnly) {
   // 当新的 VNode 不存在时,如果旧的 VNode 存在,则调用旧的 VNode 的销毁钩子函数,以确保在组件更新过程中正确地执行销毁逻辑。
   // 如果新的 VNode 不存在,通常表示组件 被移除 或者 不再需要渲染。
   // 如果旧的 VNode 仍然存在,它对应的 DOM 元素需要被删除,以保持视图与数据的同步。确保不留下无用的 DOM 节点,避免内存泄漏和不必要的性能开销。
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
    let isInitialPatch = false
    const insertedVnodeQueue: any[] = []
  // 如果 oldVnode 不存在的话,新的 vnode 是肯定存在的,比如首次渲染的时候
    if (isUndef(oldVnode)) {
      isInitialPatch = true
       // 就创建新的 vnode
      createElm(vnode, insertedVnodeQueue)
    } else {
    // 剩下的都是新的 vnode 和 oldVnode 都存在的话
    
    // 旧的 VNode是不是元素节点
      const isRealElement = isDef(oldVnode.nodeType)
     // 如果旧的 VNode 是真实的 DOM 元素节点 && 与新的 VNode 是同一个节点
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // 如果是,就用 patchVnode 对现有的根节点进行更新操作,而不是重新创建整个组件树。
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
      // 如果不是同一元素节点的话
        if (isRealElement) {
          // const SSR_ATTR = 'data-server-rendered'
          // 如果是元素节点 并且有 'data-server-rendered' 这个属性
          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 (__DEV__) {
              warn('一段很长的警告信息')
            }
          }
          // 如果不是服务端渲染的,或者混合失败,就创建一个空的注释节点替换 oldVnode
          oldVnode = emptyNodeAt(oldVnode)
        }
        // 拿到 oldVnode 的父节点
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)
         // 根据新的 vnode 创建一个 DOM 节点,挂载到父节点上
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )
       // 如果新的 vnode 的根节点存在,就是说根节点被修改了,就需要遍历更新父节点
        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)
              }
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            // 更新父节点
            ancestor = ancestor.parent
          }
        }
        // 如果旧节点还存在,就删掉旧节点
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        // 否则直接卸载 oldVnode
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    // 返回更新后的节点
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}
 
sameVnode 函数
这个是用来判断 新旧Vnode 是不是 同一节点 的函数。
function sameVnode(a, b) {
  return (
    a.key === b.key &&  // key 是不是一样
    a.asyncFactory === b.asyncFactory &&  // 是不是异步组件
    ((a.tag === b.tag &&  // 标签是不是一样
      a.isComment === b.isComment &&  // 是不是注释节点
      isDef(a.data) === isDef(b.data) &&  // 内容数据是不是一样
      sameInputType(a, b)) ||   // 判断 input 的 type 是不是一样
      (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))   // 判断区分异步组件的占位符否存在
  )
}
 
patchVnode 函数
这个是在新的 vnode 和 oldVnode 是同一节点的情况下,才会执行的函数,主要是对比 节点文本变化 或 子节点变化。
主要流程是这样的:
-  
如果 oldVnode 和 vnode 的引用地址是一样的,就表示节点没有变化,直接返回。
 -  
如果 oldVnode 的 isAsyncPlaceholder 存在,就跳过异步组件的检查,直接返回。
 -  
如果 oldVnode 和 vnode 都是静态节点 && 有相同的key && vnode是克隆节点 || v-once 指令控制的节点时,把 oldVnode.elm 和 oldVnode.child 都复制到 vnode 上,然后返回。
 -  
如果 vnode 不是文本节点 也不是注释的情况下
1)如果 oldVnode 和 vnode 都有子节点,并且
子节点不一样的时候,调用updateChildren()函数 更新子节点。
2)如果只有vnode有子节点,就调用addVnodes()创建子节点。
3)如果只有oldVnode有子节点,就调用removeVnodes()删除该子节点。
4)如果oldVnode是文本节点,就清空。 -  
如果 vnode 是文本节点但是和 oldVnode 文本内容不一样,就更新文本
 
 function patchVnode(
    oldVnode,  // 旧的虚拟 DOM 节点
    vnode,   // 新的虚拟 DOM 节点
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly?: any
  ) {
  // 新老节点引用地址是一样的,return 返回
  // 比如 props 没有改变的时候,子组件就不做渲染,直接复用
    if (oldVnode === vnode) {
      return
    }
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      vnode = ownerArray[index] = cloneVNode(vnode)
    }
    const elm = (vnode.elm = oldVnode.elm)
    
    // 如果当前节点是注释或 v-if 的,或者是异步函数,就跳过检查异步组件
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }
    // 当前节点是静态节点的时候,key 也一样,并且vnode 是克隆节点,或者有 v-once 的时候,就直接赋值返回
    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)) {
      // 但是子节点不一样,就调用 updateChildren 函数,对比子节点
        if (oldCh !== ch)
          updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(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 函数
这个是新的 vnode 和 oldVnode 都有子节点,且 子节点不一样 的时候进行对比子节点的函数。
这个函数 很关键,很关键!
比如现在有两个子节点列表对比,对比主要流程如下:
循环遍历两个列表,循环停止条件是:其中一个列表的开始指针 startIdx 和 结束指针 endIdx 重合。
 循环内容是:
- 新的头 和 老的头 对比
 - 新的尾 和 老的尾 对比
 - 新的尾 和 老的头 对比
 - 新的头 和 老的尾 对比
 
以上四种只要有一种判断相等,就调用 patchVnode() 对比节点文本变化 或 子节点变化,然后移动对比的下标,继续下一轮循环对比。
如果以上 四种情况 都没有命中,就要用 循环 来寻找了,不断拿新的节点的 key 去老的 children 里找。
-  
如果 没找到,就创建一个新的节点。
 -  
如果 找到了,再对比标签是不是同一节点。
1)如果是同一个节点,调用
pathVnode()进行后续对比,然后把这个节点插入到老的开始前面,并且移动新的开始下标,继续下一轮循环对比。
2)如果不是相同节点,就创建一个新的节点。 -  
如果老的 vnode 先遍历完,就添加 新的vnode 没有遍历的节点。
 -  
如果新的 vnode 先遍历完,说明老节点中还有剩余节点,就删除 老的vnode 没有遍历的节点。
 
为什么会有头对尾,尾对头的操作?
- 头对尾 和 尾对头是 Diff 算法的一种优化策略,目的是尽可能地
 复用已存在的 DOM 节点来减少重新渲染的成本。- 头对尾的操作指的是比较新旧节点列表中开头和结尾位置的节点对,然后逐步向内部移动比较。这样做的原因是在许多情况下,节点的变更主要发生在列表的首尾位置,而中间的节点相对稳定。通过首尾节点的对比,可以避免不必要的节点移动和更新,只需对新增或删除的节点进行插入或删除操作。
 - 尾对头的操作与头对尾类似。
 
  function updateChildren(
    parentElm,
    oldCh,
    newCh,
    insertedVnodeQueue,
    removeOnly
  ) {
    let oldStartIdx = 0  // 老 vnode 遍历的开始下标
    let newStartIdx = 0  // 新 vnode 遍历的开始下标
    let oldEndIdx = oldCh.length - 1   // 老 vnode 遍历的结束下标
    let oldStartVnode = oldCh[0]   // 老 vnode 列表第一个子元素
    let oldEndVnode = oldCh[oldEndIdx]   // 老 vnode 列表最后一个子元素
    let newEndIdx = newCh.length - 1  // 新 vnode 遍历的结束下标
    let newStartVnode = newCh[0]  // 新 vnode 列表第一个子元素
    let newEndVnode = newCh[newEndIdx]   // 老 vnode 列表最后一个子元素
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm
    const canMove = !removeOnly
    if (__DEV__) {
      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
        )
        // 然后把指针后移一位,从前往后依次对比
        // 比如第一次对比两个列表[0],然后对比[1]...,后面同理
        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)) {
        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)) {
        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)
         // 拿到新开始的 key,在老的 children 里去找有没有某个节点有这个 key
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
       // 新的 children 里有,可是没有在老的 children 里找到对应的元素
        if (isUndef(idxInOld)) {
          // 创建新的元素
          createElm(
            newStartVnode,
            insertedVnodeQueue,
            parentElm,
            oldStartVnode.elm,
            false,
            newCh,
            newStartIdx
          )
        } else {
        // 在老的 children 里找到了对应的元素
          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 {
            // 不同的话,就创建新元素
            createElm(
              newStartVnode,
              insertedVnodeQueue,
              parentElm,
              oldStartVnode.elm,
              false,
              newCh,
              newStartIdx
            )
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    
   // 说明老的 vnode 先遍历完
    if (oldStartIdx > oldEndIdx) {
    // 就添加从 newStartIdx 到 newEndIdx 之间的节点
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(
        parentElm,
        refElm,
        newCh,
        newStartIdx,
        newEndIdx,
        insertedVnodeQueue
      )
    // 否则就说明新的 vnode 先遍历完
    } else if (newStartIdx > newEndIdx) {
    // 就删除老的 vnode 里没有遍历的结点
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }
 
总结
1. 虚拟DOM的解析过程
首先对将要插入到文档中的DOM树结构进行分析,使用 js对象 将其表示出来,比如一个元素对象,包含 TagName 、props 和 Children 这些属性。然后将这个 js对象树 给保存下来,最后再将DOM片段插入到文档中。
当页面的状态发生改变,需要对页面的DOM的结构进行调整的时候,首先根据变更的状态,重新构建起一棵对象树,然后将这棵 新的对象树 和 旧的对象树 进行 比较,记录下两棵树的的差异。
最后将记录的有差异的地方应用到 真正的DOM树 中去,这样视图就更新了。
2. diff 算法的原理
在新老虚拟dom对比时:
首先,对比节点本身,通过 sameVnode() 判断是否是同一节点。
- 如果不为相同节点,则 删除 该节点重新创建新节点进行替换。
 - 如果为相同节点,进行 
patchVnode(),判断如何对该节点的子节点进行处理。 
先判断一方有子节点一方没有子节点的情况。
1)如果新的children 有子节点,就调用 addVnodes() 创建新子节点。
 2)如果新的children 没有子节点, 就调用 removeVnodes() 删除旧子节点。
如果都有子节点,但是子节点不一样时候,则进行 updateChildren(),判断如何对这些新老节点的子节点进行操作(diff 核心)。
匹配时,找到相同的子节点,递归调用 patchVnode() 函数来进一步比较和更新这些子节点。
在 diff 中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从 O(n3)降低值 O(n),也就是说,只有当新旧 children都为多个子节点时才需要用核心的 Diff 算法进行同层级比较。
可参考:
 面试中的网红虚拟DOM,你知多少呢?深入解读diff算法
 深入浅出虚拟 DOM 和 Diff 算法,及 Vue2 与 Vue3 中的区别



















