バックグラウンド
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
。
oldStartNode
およびnewStartNode
コントラストkey
値。oldEndNode
およびnewEndNode
コントラストkey
値。oldStartNode
およびnewEndNode
コントラストkey
値。newStartNode
とoldEndNode
比較key
値。
写真が示すように:
比較プロセス
次に、比較プロセスに入ります~ 比較プロセスは主に、同じキー値を持つノードを見つけることです
両端比較の実現
まず、同じノード (渡された) が見つかった場合の両端の比較について説明しますkey
。
oldStartNode
newStartNode
合計key
が同じ場合、合計は同時に 1 ビット後方にoldStartIndex
移動します。newStartIndex
oldEndNode
newEndNode
合計key
が同じ場合、合計は同時に 1 ビット進みますoldEndIndex
。newEndIndex
oldStartNode
newEndNode
合計key
が同じ場合は、oldStartIndex
1 ビット後退、newEndIndex
1 ビット前進します。oldEndNode
newStartNode
合計key
が同じ場合はoldEndIndex
1 ビット進み、newStartIndex
1 ビット後ろに進みます。
ループを終了する条件 いずれかのリスト内のすべてのノードが走査されると、比較プロセスが完了します。
これを例として考えてみましょう。
# 旧列表
a b c d
# 新列表
d b a c
初めて比較する場合:
oldStartNode A
とは異なりnewStartNode D
、比較を続けます。oldEndNode D
とは異なりnewEndNode C
、比較を続けます。oldEndNode D
がnewStartNode D
と同じである場合、古いリスト内のノード D の位置はこの時点で変更され、A の前に移動され、次にoldEndIndex
前後に移動されますnewStartIndex
。このときの仮想DOMの結果は
以下の図が得られます。
d a b c
次に、比較を続けます。
oldStartNode A
とは異なりnewStartNode B
、比較を続けます。- -
oldEndNode C
はnewEndNode C
and と同じですが、すべて末尾ノードに属しているため、ノードを直接再利用し、oldEndIndex
両方をnewEndIndex
前方に移動して
次の図を取得します。
このとき、仮想 DOM の結果は次のようになります。
d a b c
次に、比較を続けます。
oldStartNode A
とは異なりnewStartNode B
、比較を続けます。oldEndNode B
とは異なりnewEndNode A
、比較を続けます。oldStartNode A
そしてnewEndNode A
、古いリストの A ノードを B ノードの後ろに移動し、oldStartIndex
1 ビット後方に移動し、newEndIndex
1 ビット前方に移動して
、次の図を取得します。
この時点で、仮想 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
違うoldEndNode
D は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の差分アルゴリズムは後ほど書きます〜