Maison > interface Web > Voir.js > Compréhension approfondie de VNode et de l'algorithme diff dans vue2

Compréhension approfondie de VNode et de l'algorithme diff dans vue2

青灯夜游
Libérer: 2022-11-17 20:56:27
avant
1980 Les gens l'ont consulté

Compréhension approfondie de VNode et de l'algorithme diff dans vue2

L'algorithme Virtual dom et diff est un point difficile dans le processus d'apprentissage de vue, et c'est aussi un point de connaissance qui doit être maîtrisé lors de l’entretien. Les deux se complètent et constituent le cœur du framework vue. Aujourd'hui, nous allons résumer les algorithmes virtual dom et diff dans vue2. (Partage de vidéos d'apprentissage : vue vidéo tutoriel) 虚拟domdiff算法是vue学习过程中的一个难点,也是面试中必须掌握的一个知识点。这两者相辅相成,是vue框架的核心。今天我们再来总结下vue2中的虚拟domdiff算法。(学习视频分享:vue视频教程

什么是 VNode

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

比如有如下template

<template>
  <span class="demo" v-show="isShow"> This is a span. </span> 
</template>
Copier après la connexion

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

{
  tag: "span",
  data: {
    /* 指令集合数组 */
    directives: [
      {
        /* v-show指令 */
        rawName: "v-show",
        expression: "isShow",
        name: "show",
        value: true,
      },
    ],
    /* 静态class */
    staticClass: "demo",
  },
  text: undefined,
  children: [
    /* 子节点是一个文本VNode节点 */
    {
      tag: undefined,
      data: undefined,
      text: "This is a span.",
      children: undefined,
    },
  ],
};
Copier après la connexion

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

为什么vue要使用 VNode

笔者认为有两点原因

  • 由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。

  • 减少操作DOM,任何页面的变化,都只使用VNode进行操作对比,只需要在最后一次进行挂载更新DOM,避免了频繁操作DOM,减少了浏览器的回流和重绘从而提高页面性能

diff算法

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

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

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, &#39;beforeUpdate&#39;)
    }
  }
}, true /* isRenderWatcher */)
Copier après la connexion

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

_update

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

// src/core/instance/lifecycle.js

Vue.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 /* removeOnly */)
  } else {
    // 更新
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  
  // ...
}
Copier après la connexion

下面我们再来看看__patch__方法

__patch__

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

// src/core/vdom/patch.js

return 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
}
Copier après la connexion

patch方法大概流程如下:

  • 没有新节点只有老节点直接删除老节点。

  • 只有新节点没有老节点直接添加新节点。

  • 既有新节点又有老节点则判断是不是相同节点,相同则进入pathVnodepatchVnode我们后面会重点分析。

  • 既有新节点又有老节点则判断是不是相同节点,不相同则直接删除老节点添加新节点。

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

// src/core/vdom/patch.js

function 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 !== &#39;input&#39;) 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)
}
Copier après la connexion

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

  • key相同

  • 都有异步组件工厂函数

  • tag(当前节点的标签名)相同

  • isComment是否同为注释节点

  • 是否data(当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型)

  • 当标签是<input>的时候,type必须相同

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

patchVnode

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

// src/core/vdom/patch.js

function 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
  }

  /*
    如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),
    并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),
    那么只需要替换componentInstance即可。
  */
  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
  /*调用prepatch钩子*/
  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)) {
    /*新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren*/
    if (isDef(oldCh) && isDef(ch)) {
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    /*如果只有新节点有子节点,先清空elm文本内容,然后为当前DOM节点加入子节点。*/
    } else if (isDef(ch)) {
      if (process.env.NODE_ENV !== &#39;production&#39;) {
        checkDuplicateKeys(ch)
      }
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, &#39;&#39;)
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    /*如果只有老节点有子节点,则移除elm所有子节点*/
    } else if (isDef(oldCh)) {
      removeVnodes(oldCh, 0, oldCh.length - 1)
    /*当新老节点都无子节点的时候,因为这个逻辑中新节点text不存在,所以直接去除ele的文本*/
    } else if (isDef(oldVnode.text)) {
      nodeOps.setTextContent(elm, &#39;&#39;)
    }
  // 新节点是文本节点,如果文本不一样就设置新的文本  
  } else if (oldVnode.text !== vnode.text) {
    nodeOps.setTextContent(elm, vnode.text)
  }
  /*调用postpatch钩子*/
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  }
}
Copier après la connexion

patchVnode

Qu'est-ce que VNode

Nous savons que la fonction de rendu sera convertie en VNode</code >. <code>VNode est en fait un arbre basé sur des objets JavaScript. Les attributs d'objet sont utilisés pour décrire les nœuds. En fait, il s'agit simplement d'une couche de véritable DOM. abstraction. Enfin, cet arbre peut être mappé à l'environnement réel grâce à une série d'opérations.

Par exemple, il y a le modèle suivant🎜
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 !== &#39;production&#39;) {
    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)
  }
}
Copier après la connexion
Copier après la connexion
🎜S'il est remplacé par VNode, il ressemblera probablement à ce qui suit🎜rrreee🎜En général, VNode est un objet JavaScript. Cet objet JavaScript peut représenter complètement le vrai DOM. 🎜

Pourquoi vue utilise-t-il VNode

🎜L'auteur pense qu'il y a deux raisons🎜
  • 🎜Étant donné que le Virtual DOM est basé sur des objets JavaScript et ne repose pas sur l'environnement réel de la plateforme, il possède des capacités multiplateformes, comme la plateforme de navigateur, Weex, Node, etc. 🎜
  • 🎜Réduire les opérations DOM Pour toute modification de page, utilisez uniquement VNode pour la comparaison des opérations, et n'avez besoin que de monter et de mettre à jour la dernière fois<. code>DOM, évite les opérations fréquentes de DOM, réduit la redistribution et le redessin du navigateur, améliorant ainsi les performances de la page. 🎜

algorithme de comparaison

🎜Jetons un coup d'œil à la mise à jour des composants algorithme de comparaison impliqué. 🎜🎜Comme mentionné précédemment lorsque nous avons parlé de la collection de dépendances, la méthode get transmise par rendering watcher à Watcher est en fait updateComponent</ code code>méthode. 🎜rrreee🎜Le composant déclenchera donc à nouveau cette méthode lorsque les données réactives changent. Ensuite, analysons en détail la méthode <code>_update dans updateComponent. 🎜

_update

🎜Rendu initial et mise à jour dans la méthode _update La différence est que bien que la méthode __patch__ soit appelée, les paramètres passés sont différents. 🎜rrreee🎜Regardons à nouveau la méthode __patch__🎜

__patch__

🎜 La méthode patch reçoit quatre paramètres Puisque oldVnode est vm.$el lors du rendu initial, il est null</code. >, donc le rendu initial se fait sans <code>oldVnode. 🎜rrreee🎜patchLe processus approximatif de la méthode est le suivant : 🎜
  • 🎜Il n'y a pas de nouveaux nœuds, seulement d'anciens nœuds , et les anciens nœuds sont supprimés directement. 🎜
  • 🎜Seuls les nouveaux nœuds et aucun ancien nœud n'est ajouté directement. 🎜
  • 🎜S'il y a à la fois de nouveaux nœuds et d'anciens nœuds, déterminez s'il s'agit du même nœud. S'ils sont identiques, entrez pathVnode. patchVnodeNous nous concentrerons sur l'analyse plus tard. 🎜
  • 🎜S'il y a à la fois de nouveaux nœuds et d'anciens nœuds, déterminez s'il s'agit des mêmes nœuds. S'ils ne sont pas identiques, supprimez les anciens nœuds et ajoutez directement de nouveaux nœuds. 🎜
🎜Voyons comment il détermine qu'il s'agit du même nœud. 🎜rrreee🎜Pour déterminer si deux nœuds VNode sont le même nœud, les conditions suivantes doivent être remplies en même temps🎜
  • 🎜key La même🎜
  • 🎜Les deux ont des fonctions d'usine de composants asynchrones🎜
  • 🎜tag (le nom de la balise du nœud actuel) est le même🎜
  • 🎜isComment est à la fois un nœud de commentaire🎜
  • Que 🎜 soit data (l'objet correspondant au nœud actuel contient des informations de données spécifiques, c'est un type VNodeData)🎜
  • 🎜Lorsque la balise est <input>, le type</code > doit être identique🎜</li></ul> 🎜Lorsque les <code>balise, clé, isComment de deux VNode sont identiques, et les données</ code> est défini ou non défini en même temps, et si l'étiquette est <code>type d'entrée doit être la même. À l'heure actuelle, ces deux VNode sont considérés comme sameVnode, et l'opération patchVnode peut être effectuée directement. 🎜

    patchVnode

    🎜Analysons la méthode patchVnode en détail. 🎜rrreee🎜Le processus général de la méthode patchVnode est le suivant : 🎜🎜1 Si l'ancien et le nouveau nœud sont identiques, revenez directement. 🎜

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

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

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

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

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

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

    updateChildren(diff算法核心)

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

    Compréhension approfondie de VNode et de lalgorithme diff dans vue2

    这两张图代表旧的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 !== &#39;production&#39;) {
        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)
      }
    }
    Copier après la connexion
    Copier après la connexion

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

    首尾对比的四种情况

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

    • 使用旧列表的头一个节点oldStartNode新列表的头一个节点newStartNode对比

    • 使用旧列表的最后一个节点oldEndNode新列表的最后一个节点newEndNode对比

    • 使用旧列表的头一个节点oldStartNode新列表的最后一个节点newEndNode对比

    • 使用旧列表的最后一个节点oldEndNode新列表的头一个节点newStartNode对比

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

    Compréhension approfondie de VNode et de lalgorithme diff dans vue2

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

    Compréhension approfondie de VNode et de lalgorithme diff dans vue2

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

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

    Compréhension approfondie de VNode et de lalgorithme diff dans vue2

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

    Compréhension approfondie de VNode et de lalgorithme diff dans vue2

    Enfin, lorsqu'aucune des situations ci-dessus n'est rencontrée, comment gérer cette situation ?

    Trouver et comparer

    C'est rechercher et comparer.

    Tout d'abord, utilisez la méthode createKeyToOldIdx pour générer une map correspondant à la <code>clé de oldVnode et au index index.code> table. createKeyToOldIdx方法生成oldVnodekey 与 index 索引对应的一个 map 表。

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

    这里又分三种情况

    • 如果没有找到相同的节点,则通过 createElm 创建一个新节点,并将 newStartIdx 向后移动一位。

    • 如果找到了节点,同时它符合 sameVnode,则将这两个节点进行 patchVnode,将该位置的老节点赋值 undefined(之后如果还有新节点与该节点key相同可以检测出来提示已有重复的 key ),同时将 newStartVnode.elm 插入到 oldStartVnode.elm 的前面。同理,newStartIdx 往后移动一位。

    Compréhension approfondie de VNode et de lalgorithme diff dans vue2

    • 如果不符合 sameVnode,只能创建一个新节点插入到 parentElm 的子节点中,newStartIdx 往后移动一位。

    Compréhension approfondie de VNode et de lalgorithme diff dans vue2

    添加、删除节点

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

    Compréhension approfondie de VNode et de lalgorithme diff dans vue2

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

    Compréhension approfondie de VNode et de lalgorithme diff dans vue2

    总结

    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

    Ensuite, nous pouvons obtenir rapidement la même key à partir de oldKeyToIdx (la valeur de retour de createKeyToOldIdx) basée sur newStartVnode.key code> L'index du nœud code> est <code>idxInOld, puis le même nœud est trouvé. 🎜🎜Il y a trois situations ici🎜
    • 🎜Si le même nœud n'est pas trouvé, créez un nouveau nœud via createElm, Et déplacer newStartIdx d'une position vers l'arrière. 🎜
    • 🎜Si un nœud est trouvé et qu'il est conforme à sameVnode, alors effectuez patchVnode sur ces deux nœuds et attribuez l'ancien nœud à cette positionundefined
    (S'il y a un nouveau nœud avec la même clé que ce nœud plus tard, il peut être détecté et averti qu'il y a une clé en double), et en même temps, insérez newStartVnode.elm dans oldStartVnode.elm devant . De la même manière, newStartIdx recule d'une position. 🎜🎜Compréhension approfondie de VNode et de lalgorithme diff dans vue2🎜
    • 🎜S'il ne correspond pas à sameVnode, vous pouvez uniquement créer un nouveau nœud et l'insérer dans parentElm, <code>newStartIdx recule d'une position. 🎜
    🎜Compréhension approfondie de VNode et de lalgorithme diff dans vue2🎜

    🎜Ajouter et supprimer des nœuds🎜🎜🎜La dernière étape est très simple Lorsque la boucle while se termine, si . oldStartIdx > oldEndIdx, indiquant que la comparaison des anciens nœuds est terminée, mais qu'il reste encore de nombreux nouveaux nœuds à insérer dans le vrai DOM. ces nœuds. 🎜🎜Compréhension approfondie de VNode et de lalgorithme diff dans vue2🎜🎜Idem Si les conditions de newStartIdx > newEndIdx sont remplies, cela signifie que la comparaison des nouveaux nœuds est terminée et qu'il reste encore de nombreux anciens nœuds. Ces anciens nœuds inutiles peuvent être supprimés par lots via removeVnodes. . 🎜🎜Compréhension approfondie de VNode et de lalgorithme diff dans vue2🎜

    🎜Résumé🎜

    🎜🎜L'algorithme Diff est un algorithme de comparaison🎜. Comparez les deux ancien DOM virtuel et le nouveau DOM virtuel, comparez quel nœud virtuel a changé, trouvez ce nœud virtuel et mettez à jour uniquement le nœud réel correspondant à ce nœud virtuel, au lieu de mettre à jour d'autres nœuds dont les données n'ont pas changé, pour parvenir à mettre à jour avec précision le vrai DOM, améliorant ainsi l'efficacité et les performances . 🎜🎜Précision se reflète principalement dans le fait que l'algorithme diff trouve d'abord des 🎜nœuds réutilisables🎜, puis 🎜les déplace vers la bonne position🎜. Lorsque l'élément n'est pas trouvé, créez un nouveau nœud. 🎜

    🎜Extension🎜

    🎜Pourquoi devez-vous utiliser la clé dans vue et quel est son rôle ? 🎜

    🎜key est la seule balise de vnode dans Vue Grâce à cette key, Les opérations diff peuvent être plus précises et plus rapides. 🎜<ol> <li>Plus précis : car avec <code>key il n'est pas réutilisé sur place Dans la fonction sameNode a.key === b.key. Cela peut éviter la réutilisation sur place lors de la comparaison. Ce sera donc plus précis.
  • Plus rapide : utilisez le caractère unique de key pour générer un objet map afin d'obtenir le nœud correspondant, ce qui est plus rapide que la méthode de traversée.
  • 🎜Pourquoi n'est-il pas recommandé d'utiliser l'index comme clé🎜

    🎜Lorsque notre liste implique uniquement l'affichage, elle n'implique pas de tri, suppression, il n'y a aucun problème à utiliser index comme key lors de l'ajout. Parce que l'index à ce moment est unique sur chaque élément. 🎜🎜Mais s'il s'agit de trier, supprimer ou ajouter, vous ne pouvez plus utiliser index comme clé, car la clé de chaque élément n'est plus la seulement un. Une clé non unique n'aidera pas l'algorithme diff. L'écrire revient à ne pas l'écrire. 🎜

    (Partage de vidéos d'apprentissage : Développement web front-end, Vidéo de programmation de base)

    Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:juejin.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal