文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

vue2中的VNode和diff算法怎么使用

2023-07-04 15:25

关注

本文小编为大家详细介绍“vue2中的VNode和diff算法怎么使用”,内容详细,步骤清晰,细节处理妥当,希望这篇“vue2中的VNode和diff算法怎么使用”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。

什么是 VNode

我们知道,render function 会被转化成 VNodeVNode 其实就是一棵以 JavaScript 对象作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。

比如有如下template

<template>  <span class="demo" v-show="isShow"> This is a span. </span> </template>

它换成 VNode 以后大概就是下面这个样子

{  tag: "span",  data: {        directives: [      {                rawName: "v-show",        expression: "isShow",        name: "show",        value: true,      },    ],        staticClass: "demo",  },  text: undefined,  children: [        {      tag: undefined,      data: undefined,      text: "This is a span.",      children: undefined,    },  ],};

总的来说,VNode 就是一个 JavaScript 对象。这个JavaScript 对象能完整地表示出真实DOM

为什么vue要使用 VNode

笔者认为有两点原因

diff算法

下面我们来看看组件更新所涉及到的diff算法

前面我们讲依赖收集的时候有说到,渲染watcher传递给Watcherget方法其实是updateComponent方法。

updateComponent = () => {  vm._update(vm._render(), hydrating)}new Watcher(vm, updateComponent, noop, {  before () {    if (vm._isMounted) {      callHook(vm, 'beforeUpdate')    }  }}, true )

所以组件在响应式数据发生变化的时候会再次触发该方法,接下来我们来详细分析一下updateComponent里面的_update方法。

_update

_update方法中做了初始渲染和更新的区分,虽然都是调用__patch__方法,但是传递的参数不一样。

// src/core/instance/lifecycle.jsVue.prototype._update = function (vnode: VNode, hydrating?: boolean) {  const vm: Component = this  const prevEl = vm.$el  const prevVnode = vm._vnode  vm._vnode = vnode  // 初次渲染没有 prevVnode,组件更新才会有  if (!prevVnode) {    // 初次渲染    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false )  } else {    // 更新    vm.$el = vm.__patch__(prevVnode, vnode)  }    // ...}

下面我们再来看看__patch__方法

__patch__

patch方法接收四个参数,由于初始渲染的时候oldVnodevm.$elnull,所以初始渲染是没有oldVnode

// src/core/vdom/patch.jsreturn function patch (oldVnode, vnode, hydrating, removeOnly) {  // 新节点不存在,只有oldVnode就直接销毁,然后返回  if (isUndef(vnode)) {    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)    return  }  let isInitialPatch = false  const insertedVnodeQueue = []  // 没有老节点,直接创建,也就是初始渲染  if (isUndef(oldVnode)) {    isInitialPatch = true    createElm(vnode, insertedVnodeQueue)  } else {    const isRealElement = isDef(oldVnode.nodeType)    // 不是真实dom,并且是相同节点走patch    if (!isRealElement && sameVnode(oldVnode, vnode)) {      // 这里才会涉及到diff算法      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)    } else {      if (isRealElement) {        // ...      }      // replacing existing element      const oldElm = oldVnode.elm      const parentElm = nodeOps.parentNode(oldElm)      // 1.创建一个新节点      createElm(        vnode,        insertedVnodeQueue,        // extremely rare edge case: do not insert if old element is in a        // leaving transition. Only happens when combining transition +        // keep-alive + HOCs. (#4590)        oldElm._leaveCb ? null : parentElm,        nodeOps.nextSibling(oldElm)      )      // 2.更新父节点占位符      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) {              // start at index 1 to avoid re-invoking component mounted hook              for (let i = 1; i < insert.fns.length; i++) {                insert.fns[i]()              }            }          } else {            registerRef(ancestor)          }          ancestor = ancestor.parent        }      }      // 3.删除老节点      if (isDef(parentElm)) {        removeVnodes([oldVnode], 0, 0)      } else if (isDef(oldVnode.tag)) {        invokeDestroyHook(oldVnode)      }    }  }      //触发插入钩子  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)  return vnode.elm}

patch方法大概流程如下:

我们再来看看它是怎么判断是同一个节点的。

// src/core/vdom/patch.jsfunction sameVnode (a, b) {  return (    a.key === b.key &&    a.asyncFactory === b.asyncFactory && (      (        a.tag === b.tag &&        a.isComment === b.isComment &&        isDef(a.data) === isDef(b.data) &&        sameInputType(a, b)      ) || (        isTrue(a.isAsyncPlaceholder) &&        isUndef(b.asyncFactory.error)      )    )  )}function sameInputType (a, b) {  if (a.tag !== 'input') return true  let i  const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type  const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type  return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)}

判断两个VNode节点是否是同一个节点,需要同时满足以下条件

当两个VNodetag、key、isComment都相同,并且同时定义或未定义data的时候,且如果标签为input则type必须相同。这时候这两个VNode则算sameVnode,可以直接进行patchVnode操作。

patchVnode

下面我们再来详细分析下patchVnode方法。

// src/core/vdom/patch.jsfunction patchVnode (  oldVnode,  vnode,  insertedVnodeQueue,  ownerArray,  index,  removeOnly) {  // 两个vnode相同则直接返回  if (oldVnode === vnode) {    return  }  if (isDef(vnode.elm) && isDef(ownerArray)) {    // clone reused vnode    vnode = ownerArray[index] = cloneVNode(vnode)  }  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  }    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)) {      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)        } else if (isDef(ch)) {      if (process.env.NODE_ENV !== 'production') {        checkDuplicateKeys(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)  }}

patchVnode方法大概流程如下:

新老节点相同就直接返回。

如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),那么只需要替换componentInstance即可。

新节点不是文本节点,新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren,这个updateChildrendiff算法的核心,后面我们会重点说。

新节点不是文本节点,如果老节点没有子节点而新节点存在子节点,先清空老节点DOM的文本内容,然后为当前DOM节点加入子节点。

新节点不是文本节点,当新节点没有子节点而老节点有子节点的时候,则移除该DOM节点的所有子节点。

新节点不是文本节点,并且新老节点都无子节点的时候,只需要将老节点文本清空。

新节点是文本节点,并且新老节点文本不一样,则进行文本的替换。

updateChildren(diff算法核心)

updateChildrendiff算法的核心,下面我们来重点分析。

vue2中的VNode和diff算法怎么使用

这两张图代表旧的VNode与新VNode进行patch的过程,他们只是在同层级的VNode之间进行比较得到变化(相同颜色的方块代表互相进行比较的VNode节点),然后修改变化的视图,所以十分高效。所以Diff算法是:深度优先算法。 时间复杂度:O(n)

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  const canMove = !removeOnly  if (process.env.NODE_ENV !== 'production') {    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]    // 老 VNode 节点的头部与新 VNode 节点的头部是相同的 VNode 节点,直接进行 patchVnode,同时 oldStartIdx 与 newStartIdx 向后移动一位。    } else if (sameVnode(oldStartVnode, newStartVnode)) {      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)      oldStartVnode = oldCh[++oldStartIdx]      newStartVnode = newCh[++newStartIdx]    // 两个 VNode 的结尾是相同的 VNode,同样进行 patchVnode 操作。并将 oldEndVnode 与 newEndVnode 向前移动一位。    } else if (sameVnode(oldEndVnode, newEndVnode)) {      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)      oldEndVnode = oldCh[--oldEndIdx]      newEndVnode = newCh[--newEndIdx]    // oldStartVnode 与 newEndVnode 符合 sameVnode 的时候,    // 将 oldStartVnode.elm 这个节点直接移动到 oldEndVnode.elm 这个节点的后面即可。    // 然后 oldStartIdx 向后移动一位,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]    // oldEndVnode 与 newStartVnode 符合 sameVnode 时,    // 将 oldEndVnode.elm 插入到 oldStartVnode.elm 前面。    // oldEndIdx 向前移动一位,newStartIdx 向后移动一位。    } 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 {      // createKeyToOldIdx 的作用是产生 key 与 index 索引对应的一个 map 表      if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)      idxInOld = isDef(newStartVnode.key)        ? oldKeyToIdx[newStartVnode.key]        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)      // 如果没有找到相同的节点,则通过 createElm 创建一个新节点,并将 newStartIdx 向后移动一位。      if (isUndef(idxInOld)) { // New element        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)      } else {        vnodeToMove = oldCh[idxInOld]        // 如果找到了节点,同时它符合 sameVnode,则将这两个节点进行 patchVnode,将该位置的老节点赋值 undefined        // 同时将 newStartVnode.elm 插入到 oldStartVnode.elm 的前面        if (sameVnode(vnodeToMove, newStartVnode)) {          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)          oldCh[idxInOld] = undefined          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)        } else {          // 如果不符合 sameVnode,只能创建一个新节点插入到 parentElm 的子节点中,newStartIdx 往后移动一位。          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)        }      }      newStartVnode = newCh[++newStartIdx]    }  }  // 当 while 循环结束以后,如果 oldStartIdx > oldEndIdx,说明老节点比对完了,但是新节点还有多的,  // 需要将新节点插入到真实 DOM 中去,调用 addVnodes 将这些节点插入即可。  if (oldStartIdx > oldEndIdx) {    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)  // 如果满足 newStartIdx > newEndIdx 条件,说明新节点比对完了,老节点还有多,  // 将这些无用的老节点通过 removeVnodes 批量删除即可。  } else if (newStartIdx > newEndIdx) {    removeVnodes(oldCh, oldStartIdx, oldEndIdx)  }}

vue2diff算法采用的是双端比较,所谓双端比较就是新列表旧列表两个列表的头与尾互相对比,在对比的过程中指针会逐渐向内靠拢,直到某一个列表的节点全部遍历过,对比停止。

首尾对比的四种情况

我们首先来看看首尾对比的四种情况。

首先是 oldStartVnodenewStartVnode 符合 sameVnode 时,说明老 VNode 节点的头部与新 VNode 节点的头部是相同的 VNode 节点,直接进行 patchVnode,同时 oldStartIdxnewStartIdx 向后移动一位。

vue2中的VNode和diff算法怎么使用

其次是 oldEndVnodenewEndVnode 符合 sameVnode,也就是两个 VNode 的结尾是相同的 VNode,同样进行 patchVnode 操作并将 oldEndVnodenewEndVnode 向前移动一位。

vue2中的VNode和diff算法怎么使用

接下来是两种交叉的情况。

先是 oldStartVnodenewEndVnode 符合 sameVnode 的时候,也就是老 VNode 节点的头部与新 VNode 节点的尾部是同一节点的时候,将 oldStartVnode.elm 这个节点直接移动到 oldEndVnode.elm 这个节点的后面即可。然后 oldStartIdx 向后移动一位,newEndIdx 向前移动一位。

vue2中的VNode和diff算法怎么使用

同理,oldEndVnodenewStartVnode 符合 sameVnode 时,也就是老 VNode 节点的尾部与新 VNode 节点的头部是同一节点的时候,将 oldEndVnode.elm 插入到 oldStartVnode.elm 前面。同样的,oldEndIdx 向前移动一位,newStartIdx 向后移动一位。

vue2中的VNode和diff算法怎么使用

最后是当以上情况都不符合的时候,这种情况怎么处理呢?

查找对比

那就是查找对比。

首先通过createKeyToOldIdx方法生成oldVnodekeyindex 索引对应的一个 map 表。

然后我们根据newStartVnode.key,可以快速地从 oldKeyToIdxcreateKeyToOldIdx 的返回值)中获取相同 key 的节点的索引 idxInOld,然后找到相同的节点。

这里又分三种情况

vue2中的VNode和diff算法怎么使用

vue2中的VNode和diff算法怎么使用

添加、删除节点

最后一步就很容易啦,当 while 循环结束以后,如果 oldStartIdx > oldEndIdx,说明老节点比对完了,但是新节点还有多的,需要将新节点插入到真实 DOM 中去,调用 addVnodes 将这些节点插入即可。

vue2中的VNode和diff算法怎么使用

同理,如果满足 newStartIdx > newEndIdx 条件,说明新节点比对完了,老节点还有多,将这些无用的老节点通过 removeVnodes 批量删除即可。

vue2中的VNode和diff算法怎么使用

总结

Diff算法是一种对比算法。对比两者是旧虚拟DOM和新虚拟DOM,对比出是哪个虚拟节点更改了,找出这个虚拟节点,并只更新这个虚拟节点所对应的真实节点,而不用更新其他数据没发生改变的节点,实现精准地更新真实DOM,进而提高效率和性能

精准主要体现在,diff 算法首先就是找到可复用的节点,然后移动到正确的位置。当元素没有找到的话再来创建新节点。

扩展

vue中为什么需要使用key,它的作用是什么?

keyVuevnode 的唯一标记,通过这个 keydiff 操作可以更准确、更快速。

  1. 更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。

  2. 更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快。

为什么不推荐使用index作为key

当我们的列表只涉及到 展示,不涉及到排序、删除、添加的时候使用index作为key是没什么问题的。因为此时的index在每个元素上是唯一的。

但是如果涉及到排序、删除、添加的时候就不能再使用index作为key了,因为每个元素key不再唯一了。不唯一的key,对diff算法没有任何帮助,写和没写是一样的。

读到这里,这篇“vue2中的VNode和diff算法怎么使用”文章已经介绍完毕,想要掌握这篇文章的知识点还需要大家自己动手实践使用过才能领会,如果想了解更多相关内容的文章,欢迎关注编程网行业资讯频道。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     801人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     348人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     311人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     432人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     220人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯