vue での diff アルゴリズムについて話す

 

1. それは何ですか?

diff アルゴリズムは、同じレベルのツリー ノードを比較するための効率的なアルゴリズムで、次の 2 つの特徴があります。

  • 比較は同じレベルでのみ実行され、クロスレベルの比較は実行されません
  • 差分比較の過程で、ループは両側から中央まで比較します

diff このアルゴリズムは多くのシナリオで適用され、  実際のノードに仮想的にレンダリングされた  古いノードと新しい ノード を比較するためにvue 使用されます domdomVNode

2. 比較方法

diff全体的な戦略は次のとおりです。深さ優先、同じレイヤーの比較

  1. 比較は同じレベルでのみ実行され、クロスレベルの比較は実行されません
  2. 比較中、ループは両側から中央に向かって閉じられます

 

vueアルゴリズムの更新の例を次に示しますdiff

古いノードと新しいVNodeノードを次の図に示します。

 

 最初のサイクルの後、古いノード D が新しいノード D と同じであることがわかり、古いノード D は最初のdiff実ノードとして直接再利用され、古いノードはendIndexC に移動され、新しいノードは startIndex Cに移動しました

 2周目以降は旧ノードの終点も新ノードの始点と同じ(どちらもC)同様に、diff 最初に作成したBノードの後ろに後から作成したCの実ノードを挿入する。同時に、古いノードは endIndex B に移動し、新しいノードは startIndex E に移動します。

 3 サイクル目で E が見つからないことがわかり、このとき、新しい実ノード E を直接作成して、2 回目に作成した C ノードの後に​​挿入するしかありません。同時に、新しいノードは startIndex A に移動します。古いノードの startIndex 合計 はendIndex 変更されません

 

4 番目のサイクルで、古いノードと新しいノード (両方とも A) の先頭が同じであることが判明したため、  diff A の実ノードが作成され、前回作成された E ノードの後ろに挿入されます。同時に、古いノードは startIndex B に移動し、新しいノードは startIndex B に移動します。

 5周目は4周目と同じ状況なので、 diff 前回作成したAノードの後ろに、後から作成したBリアルノードを挿入します。同時に、古いノードは startIndex C に移動し、新しいノードの startIndex は F に移動しました。

新しいノードは、  作成する必要があるよりも startIndex すでに大きく  、  その間のすべてのノード、つまりノード F は、F ノードに対応する実際のノードを直接作成し、それを B ノードの後ろに配置します。endIndexnewStartIdxnewEndIdx

 

3. 原理分析

データが変更されると、setメソッドが呼び出されてDep.notifyすべてのサブスクライバーに通知されWatcher、サブスクライバーは呼び出してpatch実際のDOMデータにパッチを適用し、対応するビューを更新します

ソースの場所: src/core/vdom/patch.js

function patch(oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) { // 没有新节点,直接执行destory钩子函数
        if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
        return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
        isInitialPatch = true
        createElm(vnode, insertedVnodeQueue) // 没有旧节点,直接用新节点生成dom元素
    } else {
        const isRealElement = isDef(oldVnode.nodeType)
        if (!isRealElement && sameVnode(oldVnode, vnode)) {
            // 判断旧节点和新节点自身一样,一致执行patchVnode
            patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
        } else {
            // 否则直接销毁及旧节点,根据新节点生成dom元素
            if (isRealElement) {

                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
                    }
                }
                oldVnode = emptyNodeAt(oldVnode)
            }
            return vnode.elm
        }
    }
}

patch関数の最初の 2 つのパラメーターはoldVnode sum でVnode 、それぞれ新しいノードと古いノードを表します。主に次の 4 つの判断が行われます。

  • 新しいノードはありません。古いノードのdestoryフックを直接トリガーします
  • 古いノードがないということは、ページが初期化されたばかりのときです.このとき、比較する必要はまったくなく、すべて新しく作成されているため、呼び出すだけです. createElm
  • 古いノードは新しいノード自体と同じであり、 sameVnode ノードが同じであるかどうかを判断して、同じである場合は、  patchVnode 2 つのノードを処理するために直接呼び出します
  • 古いノードは新しいノード自体とは異なります。2 つのノードが異なる場合は、新しいノードを直接作成し、古いノードを削除します。

以下、主に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)
    }
  }

patchVnode主にいくつかの判断を行いました:

  • 新しいノードがテキスト ノードであるかどうか。テキスト ノードである場合、dom直接更新のテキスト コンテンツは新しいノードのテキスト コンテンツです。
  • 新しいノードと古いノードの両方に子ノードがある場合は、子ノードを比較して更新します
  • 新しいノードだけに子ノードがあり、古いノードにはありません。したがって、比較する必要はありません。すべてのノードは真新しいため、すべての新しいノードを直接作成するだけです。新しい作成とは、すべての新しいノードを作成し、それらを親DOMノード
  • 古いノードだけに子ノードがありますが、新しいノードにはありません。つまり、更新されたページでは古いノードがすべてなくなっているので、古いノードをすべて削除する、つまり直接削除する必要がありますDOM 。

子ノードがまったく同じでない場合は、呼び出しますupdateChildren

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0 // 旧头索引
    let newStartIdx = 0 // 新头索引
    let oldEndIdx = oldCh.length - 1 // 旧尾索引
    let newEndIdx = newCh.length - 1 // 新尾索引
    let oldStartVnode = oldCh[0] // oldVnode的第一个child
    let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
    let newStartVnode = newCh[0] // newVnode的第一个child
    let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    // 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 如果oldVnode的第一个child不存在
      if (isUndef(oldStartVnode)) {
        // oldStart索引右移
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left

      // 如果oldVnode的最后一个child不存在
      } else if (isUndef(oldEndVnode)) {
        // oldEnd索引左移
        oldEndVnode = oldCh[--oldEndIdx]

      // oldStartVnode和newStartVnode是同一个节点
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // patch oldStartVnode和newStartVnode, 索引左移,继续循环
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]

      // oldEndVnode和newEndVnode是同一个节点
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // patch oldEndVnode和newEndVnode,索引右移,继续循环
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]

      // oldStartVnode和newEndVnode是同一个节点
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // patch oldStartVnode和newEndVnode
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        // 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        // oldStart索引右移,newEnd索引左移
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]

      // 如果oldEndVnode和newStartVnode是同一个节点
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // patch oldEndVnode和newStartVnode
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        // 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        // oldEnd索引左移,newStart索引右移
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]

      // 如果都不匹配
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

        // 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

        // 如果未找到,说明newStartVnode是一个新的节点
        if (isUndef(idxInOld)) { // New element
          // 创建一个新Vnode
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)

        // 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
        } else {
          vnodeToMove = oldCh[idxInOld]
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
            warn(
              'It seems there are duplicate keys that is causing an update error. ' +
              'Make sure each v-for item has a unique key.'
            )
          }

          // 比较两个具有相同的key的新节点是否是同一个节点
          //不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // patch vnodeToMove和newStartVnode
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            // 清除
            oldCh[idxInOld] = undefined
            // 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
            // 移动到oldStartVnode.elm之前
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)

          // 如果key相同,但是节点不相同,则创建一个新的节点
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
          }
        }

        // 右移
        newStartVnode = newCh[++newStartIdx]
      }
    }

whileループは、主に次の 5 つのシナリオを処理します。

  • 古いノードと新しい VNode ノード start が同じ場合、直接 、patchVnode 古いノードと新しい ノードVNode の開始インデックスが両方とも 1 増加します。
  • 古いノードと新しい VNode ノード endが同じ場合、それも直接であり 、patchVnode 古いノードと新しい ノードVNode の終了インデックスは両方とも 1 減少します。
  • 古い VNode ノードが start 新しい VNode ノード end と同じ 場合、この時点で現在の実 ノード を後ろに移動するpatchVnode 必要があり  、同時に、古い  ノードの開始インデックスが 1 増加し、  その終了インデックスが新しいノードは 1 減ります。domoldEndVnodeVNodeVNode
  • 古い VNode ノード がend 新しい VNode ノード と同じ場合start 、この時点で 、現在の実ノードを 前  に移動する patchVnode 必要があり 、 古いノードの終了インデックスは 1 減り、  新しいノードの開始インデックスは1増加domoldStartVnodeVNodeVNode
  • 上記の 4 つの状況のいずれにも該当しない場合は、再利用できる同一のノードがないことを意味し、次の 2 つの状況に分けられます。
    •  古い  値を値とし、対応するシーケンスを値として VNode 、  ハッシュ  テーブル から同じ古い ノード を見つけて  続行し、同時にこの実数を  対応する実数  の前に移動 します。keyindexvaluenewStartVnodekeyVNodepatchVnode dom oldStartVnodedom
    • 呼び出して createElm 新しい dom ノードを作成し、現在の newStartIdx 位置に配置します

まとめ

  • データが変更されると、サブスクライバーは実際のパッチwatcherを呼び出しますpatchDOM
  • 判断してisSameVnode、同じならpatchVnodeメソッドを呼ぶ
  • patchVnode次のことを行いました:
    • domと呼ばれる対応する真実を見つけるel
    • 両方にテキスト ノードがあり、等しくない場合は、elテキスト ノードをVnodeテキスト ノードに設定します。
    • oldVnode子ノードがあり、VNode子ノードがない場合は、el子ノードを削除します
    • oldVnode子ノードがない場合VNodeVNode子ノードが実現され、に追加されますel
    • 両方に子ノードがある場合は、updateChildren関数比較子ノードを実行します
  • updateChildren主に以下の操作を行いました。
    • 新旧のVNodeヘッド ポインターとテール ポインターを設定する
    • 新旧の頭と尾のポインターを比較し、ループで真ん中に移動し、状況に応じてプロセスを繰り返す呼び出し、patchVnode新しいノードを作成する patch呼び出し、ハッシュ テーブルから一致する ノードを見つけて、状況を分割する運用へcreateElemkeyVNode

おすすめ

転載: blog.csdn.net/asfasfaf122/article/details/128784248
おすすめ