Vue源码学习 - 虚拟Dom 和 diff算法

news2024/9/20 12:32:49

目录

  • 前言
  • 一、认识虚拟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:'铁锤妹妹' }
  ]
}

它的表达方式就是把每一个标签都转为一个对象,这个对象可以有三个属性:tagpropschildren

  • 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 函数

这个是新的 vnodeoldVnode 都有子节点,且 子节点不一样 的时候进行对比子节点的函数。

这个函数 很关键,很关键!

比如现在有两个子节点列表对比,对比主要流程如下:

循环遍历两个列表,循环停止条件是:其中一个列表的开始指针 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对象 将其表示出来,比如一个元素对象,包含 TagNamepropsChildren 这些属性。然后将这个 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 中的区别

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/822004.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

简要介绍 | 生成模型的演进:从自编码器(AE)到变分自编码器(VAE)和生成对抗网络(GAN),再到扩散模型

注1:本文系“简要介绍”系列之一,仅从概念上对生成模型(包括AE, VAE, GAN,以及扩散模型)进行非常简要的介绍,不适合用于深入和详细的了解。 生成模型的演进:从自编码器(AE)到变分自编码器(VAE)和生成对抗网络(GAN),再到扩散模型 一、背景介绍 生成模型在机器学习领域…

【Linux后端开发】poll/epoll多路转接IO服务器

目录 一、poll原理 二、poll实现多路转接IO服务器 三、epoll函数接口 四、epoll的工作原理 五、epoll实现多路转接IO服务器 一、poll原理 poll函数接口 #include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout);// pollfd结构 struct pollfd …

搜索二叉树_SearchBinaryTree

目录 搜索二叉树的原理 搜索二叉树的搜索时间复杂度 二叉搜索树实现_key 模型 节点 构造函数 查找 中序遍历 插入 循环 递归 删除 循环 1.删除叶子节点 2.删除有一个孩子的节点 3.左右孩子都不为空 递归 析构函数 拷贝构造 operator key_value 模型 节点 …

JDBC-笔记

JDBC 1. JDBC介绍 JDBC&#xff08;Java Database Connectivity&#xff09;是一种用于连接和操作数据库的 Java API。 通过Java操作数据库的流程 第一步&#xff1a;编写Java代码 第二步&#xff1a;Java代码将SQL发送到MySQL服务端 第三步&#xff1a;MySQL服务端接收到SQ…

ems

【python爬虫】邮政包裹物流查询 目标网站 ems 邮政快递包裹查询: https://www.ems.com.cn/ 截图 接口预览 getPic请求滑动验证码的背景图片和滑块图片&#xff0c;返回的是base64编码的图片 getLogisticsTestFlag发送验证码的验证信息 xpos为滑动的距离&#xff0c;本站没…

CUDA编译器环境配置篇

cuda教程目录 第一章 指针篇 第二章 CUDA原理篇 第三章 CUDA编译器环境配置篇 第四章 kernel函数基础篇 第五章 kernel索引(index)篇 第六章 kenel矩阵计算实战篇 第七章 kenel实战强化篇 第八章 CUDA内存应用与性能优化篇 第九章 CUDA原子(atomic)实战篇 第十章 CUDA流(strea…

CHI中的System Debug, Trace, and Monitoring

Data Source indication □ Read request的completer&#xff0c;可以在CompData, DataSepResp, SnpRespData, and SnpRespDataPtl response中的datasource域段中指定data的来源&#xff1b;即使响应中带有错误&#xff0c;该datasource也是有效的&#xff1b; □ 该域段也可复…

Flutter 之Bloc入门指南实现倒计时功能

Flutter Timer By Bloc 前言Stream.periodic实现倒计时定义Bloc状态定义Bloc事件定义Bloc组件定义View层参考资料前言 使用Bloc开发Flutter的项目,其项目结构都很简单明确,需要创建状态,创建事件,创建bloc,创建对应的View。flutter_timer项目来分析下Bloc的使用方法。 通…

逻辑回归变量系数可为负数吗?应该如何解释?

之前很多学员来问逻辑回归变量系数是否都应该为正数&#xff0c;如果出现负的变量系数该怎么办&#xff1f;是否需要重新建模&#xff1f;这些学员都是在网上搜索时&#xff0c;被错误信息误导。网上信息可以随意转载&#xff0c;且无人审核对错。我见过最多情况时很多文章正确…

软工导论知识框架(三)结构化的设计

一.传统软件工程方法学采用结构化设计技术&#xff08;SD&#xff09; 从工程管理角度结构化设计分两步&#xff1a; 概要设计&#xff1a; 将软件需求转化为数据结构和软件系统结构。详细设计&#xff1a;过程设计&#xff0c;通过对结构细化&#xff0c;得到软件详细数据结构…

dubbo-helloworld示例

1、工程架构 2、创建模块 &#xff08;1&#xff09;创建父工程,引入公共依赖 pom.xml依赖 <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></depende…

MultipartFile 获取文件名、文件前缀、文件后缀、文件类型

测试 debug 方法 RequestMapping(value "/test",method RequestMethod.POST)public void fileUpload(MultipartFile file){// 文件名String originalFilename file.getOriginalFilename();// 文件名前缀String fileName file.getOriginalFilename().substring(0,…

第5讲:VBA中OFFSET函数的利用

【分享成果&#xff0c;随喜正能量】幸福从来不是跟别人比来的&#xff0c;追求不同&#xff0c;各有活法&#xff0c;开心了就笑&#xff0c;累了就休息&#xff0c;日子安稳踏实就是最大的幸福。做人就怕尊严扫地&#xff0c;保留一点做人的尊严&#xff0c;是人生最大的本钱…

C语言每日一题

今天分享的是一道牛客网上面的题目&#xff0c;链接在下面 有序序列合并 这道题做法有很多&#xff0c;最简单的是合并一起&#xff0c;然后用排序就行了&#xff0c;今天将一个最高效的办法&#xff0c;思路是两个数组第一项进行比较&#xff0c;小的先输出&#xff0c;输出的…

Mac上命令

1. block端口&#xff1a; sudo cp /etc/pf.conf /etc/pf443.conf 编辑pf443.conf&#xff0c;vim /etc/pf443.conf&#xff0c;如 block on en0 proto udp from any to any port 9000 # block UDP port 9000 block on en0 proto tcp from any to any port 5004 # bloc…

InnoDB引擎底层逻辑讲解——后台线程

1.后台线程 后台线程的作用就是将innodb存储引擎缓冲池中的数据&#xff0c;在合适的时机刷新到磁盘文件当中。innodb存储引擎后台的线程主要分为四类&#xff1a;

Golang之路---02 基础语法——函数

函数 函数定义 func function_name( [parameter list] ) [return_types] {函数体 }参数解释&#xff1a; func&#xff1a;函数由 func 开始声明function_name&#xff1a;函数名称&#xff0c;函数名和参数列表一起构成了函数签名。[parameter list]&#xff1a;参数列表&a…

Istio 安全 mTLS认证 PeerAuthentication

这里定义了访问www.ck8s.com可以使用http也可以使用https访问&#xff0c;两种方式都可以访问。 那么是否可以强制使用mtls方式去访问&#xff1f; mTLS认证 PeerAuthentication PeerAuthentication的主要作用是别人在和网格里的pod进行通信的时候&#xff0c;是否要求mTLS mTL…

信息学奥赛一本通——1258:【例9.2】数字金字塔

文章目录 题目【题目描述】【输入】【输出】【输入样例】【输出样例】 AC代码 题目 【题目描述】 观察下面的数字金字塔。写一个程序查找从最高点到底部任意处结束的路径&#xff0c;使路径经过数字的和最大。每一步可以从当前点走到左下方的点也可以到达右下方的点。 在上面…

web前端开发工程师岗位的主要职责八篇

web前端开发工程师岗位的主要职责1 职责&#xff1a; 1、负责web前端系统和功能的开发、调试和维护&#xff0c;前端技术框架和js等互动效果开发; 2、负责公司现有项目和新项目的前端修改调试和开发工作; 3、根据工作安排高效、高质地完成代码编写&#xff0c;确保符合规范…