51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

解析Vue算法:带你们了解下虚拟DOM与Diff算法的应用

dom&diff算法

分下Vue两大算法:虚拟DOM与Diff。

虚拟DOM {#heading-0}

虚拟 DOM (Virtual DOM,简称 VDOM) 是一种编程概念,意为将目标所需的 UI 通过数据结构"虚拟"地表示出来,保存在内存中,然后将真实的DOM与之保持同步。具体来说,虚拟 DOM是由一系列的 JavaScript 对象组成的树状结构,每个对象代表着一个DOM元素,包括元素的标签名、属性、子节点等信息。虚拟 DOM中的每个节点都是一个 JavaScript 对象,它们可以轻松地被创建、更新和销毁,而不涉及到实际的DOM操作。

{#_lab2_0_0}

主要作用 {#heading-1}

虚拟 DOM的主要作用是在数据发生变化时,通过与上一次渲染的虚拟 DOM进行对比,找出发生变化的部分,并最小化地更新实际 DOM。这种方式可以减少实际 DOM操作的次数,从而提高页面渲染的性能和效率。
总的来说,虚拟 DOM是一种用 JavaScript 对象模拟真实 DOM结构和状态的技术,它通过在内存中操作虚拟 DOM 树来减少实际 DOM操作,从而提高页面的性能和用户体验。

{#_label1}

虚拟DOM树 {#heading-2}

顾名思义,也就是一个虚拟 DOM 作为根节点,包含有一个或多个的子虚拟 DOM。

{#_label2}

Diff {#heading-3}

在 Vue 3 中,diff(差异比较)是指在进行虚拟 DOM 更新时,对比新旧虚拟 DOM 树的差异,然后只对实际发生变化的部分进行更新,以尽可能地减少对真实 DOM 的操作,提高页面的性能和效率。diff整体策略为:深度优先,同层比较。也就是说,比较只会在同层级进行, 不会跨层级比较;比较的过程中,循环从两边向中间收拢。

{#_lab2_2_1}

流程解析 {#heading-4}

Diff 算法的实现流程可以概括为以下几个步骤:

  • 比较根节点: 首先,对比新旧虚拟 DOM 树的根节点,判断它们是否相同。

  • 逐层对比子节点: 如果根节点相同,则逐层对比子节点。

    • 通过双指针法对比新旧节点列表,查找相同位置的节点。

    • 如果节点相同,进行递归对比子节点。

    • 如果节点不同,根据情况执行插入、删除或移动节点的操作。

    • 如果节点类型不同,则直接替换整个节点。

    • 如果节点类型相同,继续对比节点的属性和事件。

    • 比较子节点类型:

    • 对比子节点列表:

  • 处理新增、删除和移动的节点:

    • 如果新节点列表中存在旧节点列表中没有的节点,执行新增操作。

    • 如果旧节点列表中存在新节点列表中没有的节点,执行删除操作。

    • 如果新旧节点列表中都存在相同的节点,但顺序不同,执行移动节点的操作。

  • 更新节点属性和事件:

    • 如果节点相同但属性或事件发生了变化,更新节点的属性和事件。
  • 递归对比子节点:

    • 如果节点类型相同且是容器节点(例如 div、ul 等),则递归对比子节点。

{#_label3}

源码解析 {#heading-5}

在源码中patchVnode是diff发生的地方,下面是patchVnode的源码:

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  // 如果新旧节点一致,什么都不做
  if (oldVnode === vnode) {
    return
  }

  // 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化
  const elm = vnode.elm = oldVnode.elm

  // 异步占位符
  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
    } else {
      vnode.isAsyncPlaceholder = true
    }
    return
  }
  // 如果新旧都是静态节点,并且具有相同的key
  // 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上
  // 也不用再有其他操作
  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)
  }
  // 如果vnode不是文本节点或者注释节点
  if (isUndef(vnode.text)) {
    // 并且都有子节点
    if (isDef(oldCh) && isDef(ch)) {
      // 并且子节点不完全一致,则调用updateChildren
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)

      // 如果只有新的vnode有子节点
    } else if (isDef(ch)) {
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      // elm已经引用了老的dom节点,在老的dom节点上添加子节点
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)

      // 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh
    } else if (isDef(oldCh)) {
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)

      // 如果老节点是文本节点
    } else if (isDef(oldVnode.text)) {
      nodeOps.setTextContent(elm, '')
    }

    // 如果新vnode和老vnode是文本节点或注释节点
    // 但是vnode.text != oldVnode.text时,只需要更新vnode.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)
  }
}

以上代码主要就是用于比较新旧虚拟 DOM 节点并进行更新。让我逐步解释这个函数的实现:

  • 判断是否需要更新: 首先,函数会比较新旧虚拟 DOM 节点是否相同,如果相同则直接返回,无需进行后续操作。

  • 获取旧节点的真实 DOM 引用: 通过 elm = vnode.elm = oldVnode.elm 将新节点 vnode 的真实 DOM 引用指向旧节点的真实 DOM。

  • 处理异步占位符: 如果旧节点是异步占位符(asyncPlaceholder),并且新节点的异步工厂已经解析,则通过 hydrate 函数进行同步操作;否则,将新节点标记为异步占位符并返回。

  • 处理静态节点: 如果新旧节点都是静态节点(isStatic 为真),并且具有相同的 key,则将新节点的组件实例引用指向旧节点的组件实例。

  • 触发 prepatch 钩子: 如果新节点的数据对象中定义了 hook 并且 prepatch 钩子存在,则执行该钩子函数,用于预处理新旧节点之间的差异。

  • 更新节点的属性和事件: 如果新节点的数据对象中定义了 hook 并且 update 钩子存在,则执行该钩子函数,用于更新节点的属性和事件。

  • 处理子节点: 如果新旧节点都有子节点,则比较它们之间的差异并进行更新,调用 updateChildren 函数。如果只有新节点有子节点,则将新节点的子节点添加到旧节点上。如果只有旧节点有子节点,则删除旧节点的子节点。如果旧节点是文本节点,则清空其内容。

  • 更新文本内容: 如果新旧节点都是文本节点或注释节点,并且它们的文本内容不同,则更新新节点的文本内容。

  • 触发 postpatch 钩子: 如果新节点的数据对象中定义了 hook 并且 postpatch 钩子存在,则执行该钩子函数,用于处理节点更新后的操作。

{#_lab2_3_2}

Diff算法示例 {#heading-6}

下面是一个详细的例子,假设有以下两个虚拟 DOM 树,我们将对它们进行 diff 算法的比较:

旧的虚拟 DOM 树:

{
  type: 'div',
  props: { id: 'container' },
  children: [
    { type: 'p', props: { class: 'text' }, children: ['old Dom'] },
    { type: 'button', props: { disabled: true }, children: ['click'] }
  ]
}

新的虚拟 DOM 树:

{
  type: 'div',
  props: { id: 'container' },
  children: [
    { type: 'p', props: { class: 'text' }, children: ['new DOM'] },
    { type: 'button', props: { disabled: false }, children: ['click'] },
    { type: 'span', props: { class: 'msg' }, children: ['msg'] }
  ]
}

Diff算法执行:

  • 比较根节点:根节点相同,继续比较子节点。

  • 比较子节点:

    • 第一个子节点类型相同,但内容不同,更新内容为 'new DOM'。

    • 第二个子节点相同,属性发生变化,更新 disabled 属性为 false。

    • 第三个子节点是新增节点,执行插入操作。

  • 更新节点属性和事件:第二个子节点的属性发生变化,更新 disabled 属性。

  • 递归对比子节点:针对新增的 span 节点,继续递归对比其子节点。

最终结果:

    {
  type: 'div',
  props: { id: 'container' },
  children: [
    { type: 'p', props: { class: 'text' }, children: ['new DOM'] },
    { type: 'button', props: { disabled: false }, children: ['click'] },
     { type: 'span', props: { class: 'msg' }, children: ['msg'] }
  ]
}

结语 {#heading-7}

总的来说,Diff 算法的核心思想是Diff就是将新老虚拟DOM的不同点找到并生成一个补丁,并根据这个补丁生成更新操作,以最小化对实际 DOM 的操作,提高页面渲染的性能和效率。通过深度优先、同层比较的策略,Diff 算法能够高效地处理虚拟 DOM 树的更新,使得页面在数据变化时能够快速响应并更新对应的视图。


赞(0)
未经允许不得转载:工具盒子 » 解析Vue算法:带你们了解下虚拟DOM与Diff算法的应用