Vue2 Diff アルゴリズムの簡易バージョン

バックグラウンド

Vue2両端の比較方法を見直してください~

両端の比較

Vue2 で採用されている両端比較方式、つまり、新しいリストと古いリストの先頭から末尾までの比較です。比較プロセス中、ポインタは徐々に内側に移動し、特定のリストのすべてのノードを通過し、比較が停止します。
例えば:

# 旧列表
a b c d
# 新列表
d b a c

最初にいくつかの変数を定義します。

function vue2diff(prevChildren, nextChildren, parent) {
    
    
	let
    // 旧头指针 
    oldStartIndex = 0,
    // 新头指针
    newStartIndex = 0,
    // 旧尾指针
    oldEndIndex = prevChildren.length - 1,
    // 新尾指针
    newEndIndex = nextChildren.length - 1,
    // 旧头节点
    oldStartNode = prevChildren[oldStartIndex],
    // 旧尾节点
    oldEndNode = prevChildren[oldEndIndex],
    // 新头节点
    newStartNode = nextChildren[newEndIndex],
    // 新尾节点
    newEndNode = nextChildren[newStartIndex];
}

このとき、ポインタが 4 つあり、その 4 つのポインタに対応するノードが、古いリストのノード a、古いリストのノード d、新しいリストのノード d、新しいリストのノード c になります。次の式の便宜上、古いリストのノード a は などと記述されますoldNodeA

  1. oldStartNodeおよびnewStartNodeコントラストkey値。
  2. oldEndNode およびnewEndNode コントラストkey値。
  3. oldStartNodeおよびnewEndNode コントラストkey値。
  4. newStartNodeoldEndNode 比較key値。
    写真が示すように:
    ここに画像の説明を挿入

比較プロセス

次に、比較プロセスに入ります~ 比較プロセスは主に、同じキー値を持つノードを見つけることです

両端比較の実現

まず、同じノード (渡された) が見つかった場合の両端の比較について説明しますkey

  • oldStartNodenewStartNode合計keyが同じ場合、合計は同時に 1 ビット後方にoldStartIndex移動します。newStartIndex
  • oldEndNodenewEndNode合計keyが同じ場合、合計は同時に 1 ビット進みますoldEndIndexnewEndIndex
  • oldStartNodenewEndNode合計keyが同じ場合は、 oldStartIndex1 ビット後退、newEndIndex1 ビット前進します。
  • oldEndNodenewStartNode合計keyが同じ場合はoldEndIndex1 ビット進み、newStartIndex1 ビット後ろに進みます。

ループを終了する条件 いずれかのリスト内のすべてのノードが走査されると、比較プロセスが完了します。

これを例として考えてみましょう。

# 旧列表
a b c d
# 新列表
d b a c

初めて比較する場合:
ここに画像の説明を挿入

  • oldStartNode A とは異なりnewStartNode D、比較を続けます。
  • oldEndNode Dとは異なりnewEndNode C、比較を続けます。
  • oldEndNode DnewStartNode Dと同じである場合、古いリスト内のノード D の位置はこの時点で変更され、A の前に移動され、次にoldEndIndex前後に移動されますnewStartIndexこのときの仮想DOMの結果は
    以下の図が得られます。
    ここに画像の説明を挿入
d a b c

次に、比較を続けます。

  • oldStartNode A とは異なりnewStartNode B、比較を続けます。
  • -oldEndNode CnewEndNode Cand と同じですが、すべて末尾ノードに属しているため、ノードを直接再利用し、oldEndIndex両方をnewEndIndex前方に移動して
    次の図を取得します。
    ここに画像の説明を挿入
    このとき、仮想 DOM の結果は次のようになります。
d a b c

次に、比較を続けます。

  • oldStartNode A とは異なりnewStartNode B、比較を続けます。
  • oldEndNode B とは異なりnewEndNode A、比較を続けます。
  • oldStartNode A そしてnewEndNode A、古いリストの A ノードを B ノードの後ろに移動し、oldStartIndex1 ビット後方に移動し、newEndIndex1 ビット前方に移動して
    、次の図を取得します。
    ここに画像の説明を挿入
    この時点で、仮想 DOM の結果は次のようになります。
d b a c

最後に、比較oldEndNode B と一致しnewEndNode Bてサイクルが終了します。
一般的なコードは次のとおりです。

function vue2diff(prevChildren, nextChildren, parent) {
    
    
  // ...
  // 双端对比,当有一个列表的节点全部遍历完成,则结束循环
  while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
    
    
    if (oldStartNode.key === newStartNode.key) {
    
    
      // 当两个头节点相同时,节点不需要处理,两个指针的坐标向后移动一位
      patch(oldStartNode, newStartNode, parent)

      oldStartIndex++
      newStartIndex++
      oldStartNode = prevChildren[oldStartIndex]
      newStartNode = nextChildren[newStartIndex]
    } else if (oldEndNode.key === newEndNode.key) {
    
    
      // 当两个尾节点相同时,节点不需要处理,两个指针的坐标向前移动一位
      patch(oldEndNode, newEndNode, parent)

      oldEndIndex--
      newEndIndex--
      oldEndNode = prevChildren[oldEndIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldStartNode.key === newEndNode.key) {
    
    
      // 当旧头节点跟新尾节点相同时,需要移动旧头节点,旧指针向后移动一位,新指针向前移动一位
      patch(oldStartNode, newEndNode, parent)
      parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
      oldStartIndex++
      newEndIndex--
      oldStartNode = prevChildren[oldStartIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldEndNode.key === newStartNode.key) {
    
    
      // 当旧尾节点和新头节点相同时,需要移动旧头节点,旧指针向前移动一位,新指针向后移动一位
      patch(oldEndNode, newStartNode, parent)
      parent.insertBefore(oldEndNode.el, oldStartNode.el)
      oldEndIndex--
      newStartIndex++
      oldEndNode = prevChildren[oldEndIndex]
      newStartNode = nextChildren[newStartIndex]
    } else {
    
    
      // ...
    }
  }
}

上記は新旧ノードの先頭と末尾の比較で同じノードが出現する状況ですが、次に4つの比較で再利用ノードが見つからない状況を見てみましょう。
ここに画像の説明を挿入

  • oldStartNode AとはnewStartNode E違う
  • oldEndNode DとはnewEndNode H違う
  • oldStartNode AとはnewEndNode H違う
  • oldEndNodeD はnewStartNode E次とは異なります。

このとき、まず新しいリストの最初のノードを取り出し、次に古いリストに再利用できるノードがあるかどうかを調べます。ここには 2 つの状況があります。最初に図の 1 つについて説明します。存在しない場合はoldStartNode A、これは新しいノードであることを意味し、古いリストの先頭に直接配置できます

ここに画像の説明を挿入

ここに画像の説明を挿入

function vue2diff(prevChildren, nextChildren, parent) {
    
    
  // ...
  // 双端对比,当有一个列表的节点全部遍历完成,则结束循环
  while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
    
    
    if (oldStartNode.key === newStartNode.key) {
    
    
      // 当两个头节点相同时,节点不需要处理,两个指针的坐标向后移动一位
      patch(oldStartNode, newStartNode, parent)

      oldStartIndex++
      newStartIndex++
      oldStartNode = prevChildren[oldStartIndex]
      newStartNode = nextChildren[newStartIndex]
    } else if (oldEndNode.key === newEndNode.key) {
    
    
      // 当两个尾节点相同时,节点不需要处理,两个指针的坐标向前移动一位
      patch(oldEndNode, newEndNode, parent)

      oldEndIndex--
      newEndIndex--
      oldEndNode = prevChildren[oldEndIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldStartNode.key === newEndNode.key) {
    
    
      // 当旧头节点跟新尾节点相同时,需要移动旧头节点,旧指针向后移动一位,新指针向前移动一位
      patch(oldStartNode, newEndNode, parent)
      parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
      oldStartIndex++
      newEndIndex--
      oldStartNode = prevChildren[oldStartIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldEndNode.key === newStartNode.key) {
    
    
      // 当旧尾节点和新头节点相同时,需要移动旧头节点,旧指针向前移动一位,新指针向后移动一位
      patch(oldEndNode, newStartNode, parent)
      parent.insertBefore(oldEndNode.el, oldStartNode.el)
      oldEndIndex--
      newStartIndex++
      oldEndNode = prevChildren[oldEndIndex]
      newStartNode = nextChildren[newStartIndex]
    } else {
    
    
      // 四个节点对比的过程中没有发现相同的节点时
      // 先看看新列表的头节点是否存在于旧列表中
      let newKey = newStartNode.key,
        oldIndex = prevChildren.findIndex(child => child && (child.key === newKey));
      // 如果不存在,直接创建放在最前方即可
      if (oldIndex === -1) {
    
    
        mount(newStartNode, parent, oldStartNode.el)
      } else {
    
    
      // 如果存在,将旧节点移动到第一个节点,并在旧列表中置成undefine, 跳过对比过程
        let prevNode = prevChildren[oldIndex]
        patch(prevNode, newStartNode, parent)
        parent.insertBefore(prevNode.el, oldStartNode.el)
        prevChildren[oldIndex] = undefined
      }
      // 更新新列表的对比节点
      newStartIndex++
      newStartNode = nextChildren[newStartIndex]
    }
  }
}

現時点で考慮する必要がある問題は、次のサイクルに入るときに、対応する oldStartNode が未定義であるため、コードを処理する必要があることです。

function vue2diff(prevChildren, nextChildren, parent) {
    
    
  let
    // 旧头指针 
    oldStartIndex = 0,
    // 新头指针
    newStartIndex = 0,
    // 旧尾指针
    oldEndIndex = prevChildren.length - 1,
    // 新尾指针
    newEndIndex = nextChildren.length - 1,
    // 旧头节点
    oldStartNode = prevChildren[oldStartIndex],
    // 旧尾节点
    oldEndNode = prevChildren[oldEndIndex],
    // 新头节点
    newStartNode = nextChildren[newEndIndex],
    // 新尾节点
    newEndNode = nextChildren[newStartIndex];
  // 双端对比,当有一个列表的节点全部遍历完成,则结束循环
  while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
    
    
    // 旧列表中遇到Undefine节点则跳过对比
    if (oldStartNode === undefined) {
    
    
      oldStartNode = prevChildren[++oldStartIndex]
    } else if (oldEndNode === undefined) {
    
    
      oldEndNode = prevChildren[--oldStartIndex]
    } else if (oldStartNode.key === newStartNode.key) {
    
    
      // ...
    } else if (oldEndNode.key === newEndNode.key) {
    
    
      // ...
    } else if (oldStartNode.key === newEndNode.key) {
    
    
      // ...
    } else if (oldEndNode.key === newStartNode.key) {
    
    
      // ...
    } else {
    
    
      // ...
    }
  }
  
}

次に、リストの削除の場合を考えてみましょう。
ここに画像の説明を挿入

  • ノード A の多重化。
  • ノードDは多重化されており、
    このとき各座標は以下のように変化します。
    ここに画像の説明を挿入
// 当新列表的newStartIndex 大于newEndIndex,说明新列表删除的节点
  if (newStartIndex > newEndIndex) {
    
    
    while (oldStartIndex <= oldStartIndex) {
    
    
      if (!prevChildren[oldStartIndex]) {
    
    
        oldStartIndex++
        continue
      }
      // 删除节点
      parent.removeChild(prevChildren[oldStartIndex++].el)
    }
  } 

いよいよノード追加です~
ここに画像の説明を挿入

  • ノード A の多重化。
  • ノードBは多重化されており、
    このとき各座標は以下のように変化します。
    ここに画像の説明を挿入
if (oldStartIndex> oldEndIndex ) {
    
    
    // 当旧列表的oldEndIndex 小于oldStartIndex,说明新列表新增了节点
    for (let i = newStartIndex; i <= newEndIndex; i++) {
    
    
      mount(nextChildren[i], parent, prevStartNode.el)
    }
  }

最後に、完全なコードを貼り付けます。

function vue2diff(prevChildren, nextChildren, parent) {
    
    
  let
    // 旧头指针 
    oldStartIndex = 0,
    // 新头指针
    newStartIndex = 0,
    // 旧尾指针
    oldEndIndex = prevChildren.length - 1,
    // 新尾指针
    newEndIndex = nextChildren.length - 1,
    // 旧头节点
    oldStartNode = prevChildren[oldStartIndex],
    // 旧尾节点
    oldEndNode = prevChildren[oldEndIndex],
    // 新头节点
    newStartNode = nextChildren[newEndIndex],
    // 新尾节点
    newEndNode = nextChildren[newStartIndex];
  // 双端对比,当有一个列表的节点全部遍历完成,则结束循环
  while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
    
    
    // 旧列表中遇到Undefine节点则跳过对比
    if (oldStartNode === undefined) {
    
    
      oldStartNode = prevChildren[++oldStartIndex]
    } else if (oldEndNode === undefined) {
    
    
      oldEndNode = prevChildren[--oldStartIndex]
    } else if (oldStartNode.key === newStartNode.key) {
    
    
      // 当两个头节点相同时,节点不需要处理,两个指针的坐标向后移动一位
      patch(oldStartNode, newStartNode, parent)

      oldStartIndex++
      newStartIndex++
      oldStartNode = prevChildren[oldStartIndex]
      newStartNode = nextChildren[newStartIndex]
    } else if (oldEndNode.key === newEndNode.key) {
    
    
      // 当两个尾节点相同时,节点不需要处理,两个指针的坐标向前移动一位
      patch(oldEndNode, newEndNode, parent)

      oldEndIndex--
      newEndIndex--
      oldEndNode = prevChildren[oldEndIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldStartNode.key === newEndNode.key) {
    
    
      // 当旧头节点跟新尾节点相同时,需要移动旧头节点,旧指针向后移动一位,新指针向前移动一位
      patch(oldStartNode, newEndNode, parent)
      parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
      oldStartIndex++
      newEndIndex--
      oldStartNode = prevChildren[oldStartIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldEndNode.key === newStartNode.key) {
    
    
      // 当旧尾节点和新头节点相同时,需要移动旧头节点,旧指针向前移动一位,新指针向后移动一位
      patch(oldEndNode, newStartNode, parent)
      parent.insertBefore(oldEndNode.el, oldStartNode.el)
      oldEndIndex--
      newStartIndex++
      oldEndNode = prevChildren[oldEndIndex]
      newStartNode = nextChildren[newStartIndex]
    } else {
    
    
      // 四个节点对比的过程中没有发现相同的节点时
      // 先看看新列表的头节点是否存在于旧列表中
      let newKey = newStartNode.key,
        oldIndex = prevChildren.findIndex(child => child && (child.key === newKey));
      // 如果不存在,直接创建放在最前方即可
      if (oldIndex === -1) {
    
    
        mount(newStartNode, parent, oldStartNode.el)
      } else {
    
    
      // 如果存在,将旧节点移动到第一个节点,并在旧列表中置成undefine, 跳过对比过程
        let prevNode = prevChildren[oldIndex]
        patch(prevNode, newStartNode, parent)
        parent.insertBefore(prevNode.el, oldStartNode.el)
        prevChildren[oldIndex] = undefined
      }
      // 更新新列表的对比节点
      newStartIndex++
      newStartNode = nextChildren[newStartIndex]
    }
  }
  // 当新列表的newStartIndex 大于newEndIndex,说明新列表删除的节点
  if (newStartIndex > newEndIndex) {
    
    
    while (oldStartIndex <= oldStartIndex) {
    
    
      if (!prevChildren[oldStartIndex]) {
    
    
        oldStartIndex++
        continue
      }
      parent.removeChild(prevChildren[oldStartIndex++].el)
    }
  } else if (oldStartIndex> oldEndIndex ) {
    
    
    // 当旧列表的oldEndIndex 小于oldStartIndex,说明新列表新增了节点
    for (let i = newStartIndex; i <= newEndIndex; i++) {
    
    
      mount(nextChildren[i], parent, prevStartNode.el)
    }
  }
}

まとめ

Vue2 の差分アルゴリズムを再学習しました: 両端比較方法。新しい利点があり、考え方がより明確になりました。Vue3の差分アルゴリズムは後ほど書きます〜

参考リンク

おすすめ

転載: blog.csdn.net/qq_34086980/article/details/131552585