1 はじめに
前回の記事では、新しいノードVNode
と古いノードの両方がoldVNode
要素ノードであり、両方に子ノードが含まれる場合、Vue
その子ノードは
最初に外側のループnewChildren
配列、次に内側のループ配列。外側の配列の子ノードがループされるoldChildren
たびに、内側の配列に移動してそれと同じ子ノードがあるかどうかを確認し、最後に別の操作を実行します。さまざまな状況に応じて。newChildren
oldChildren
前回の記事の最後で、この方法で問題は解決できるものの、まだ最適化できる箇所があることも述べました。たとえば、子ノードの数が多い場合、ループ アルゴリズムの時間計算量が非常に大きくなり、パフォーマンスの向上にはつながりません。もちろん私もこれに気づいて最適化しましたので、この記事ではVue
子ノードの更新を最適化するVue
方法を学びましょう。
2. 最適化戦略の概要
次のように、新しいnewChildren
配列と古いoldChildren
配列があるとします。
newChildren = ['新子节点1','新子节点2','新子节点3','新子节点4']
oldChildren = ['旧子节点1','旧子节点2','旧子节点3','旧子节点4']
最適化の前に解決策に従う場合、次の操作は次のようになります。まずnewChildren
配列をループし、最初の新しい子ノード 1 を取得し、次に最初の新しい子ノード 1 を使用してoldChildren
配列内の古い子ノードをたどります。運が少しよければ、oldChildren
配列内の最初の古い子ノード 1 が最初の新しい子ノード 1 と偶然同じであれば、全員が満足し、さらにループダウンせずに直接処理できます。次に、運が少し悪い場合は、oldChildren
ループ内の 4 番目の古い子ノード 4 が最初の新しい子ノード 1 と同じになるまで、この時点でさらに 4 回ループします。もう少し極端な状況を想像してみてもよいでしょう。newChildren
配列もoldChildren
配列内の最初の 3 つのノードも変更されていないが、4 番目のノードだけが変更されている場合は、16 回ループし、16 番目でのみ判明します。以下の図に示すように、新しいノード 4 は古いノード 4 と同じであり、更新する必要があります。
上記の例では、子ノードが 4 つしかないため、問題がないように見えますが、子ノードの数が多くなると、アルゴリズムの時間計算量が非常に高くなり、パフォーマンスの向上にはつながりません。 。
では、どうすれば最適化できるのでしょうか? 実際、このように考えることができます。2 つの配列を順番にループする代わりにnewChildren
、oldChildren
最初に 2 つの配列内の特別な位置にある子ノードを比較できます。次に例を示します。
- まず、
newChildren
配列内のすべての未処理の子ノードの最初の子ノードと、配列内のすべての未処理の子ノードの最初の子ノードoldChildren
を比較し、それらが同じであれば、直接ノードの更新操作に入ります。 - 異なる場合は、
newChildren
配列内のすべての未処理の子ノードの最後の子ノードと、oldChildren
配列内のすべての未処理の子ノードの最後の子ノードを比較し、同じであれば、直接ノードの更新操作に入ります。 - それらが異なる場合は、
newChildren
配列内のすべての未処理の子ノードの最後の子ノードと、配列内のすべての未処理の子ノードの最初の子ノードoldChildren
を比較します。それらが同じである場合は、ノードの更新操作に直接進みます。更新後次に、oldChildren
配列内のノードをnewChildren
配列内のノードと同じ位置に移動します。 - それらが異なる場合は、配列
newChildren
内のすべての未処理の子ノードの最初の子ノードoldChildren
と、配列内のすべての未処理の子ノードの最後の子ノードを比較し、それらが同じである場合は、ノードの更新操作を直接入力します。oldChildren
配列内のノードをnewChildren
配列内のノードと同じ位置に移動します。 - 最後の 4 つのケースを試した後、それでも異なる場合は、前のループ方法に従ってノードを見つけます。
プロセスを次の図に示します。
上の画像では、次のように配置します。
newChildren
配列内のすべての未処理の子ノードの最初の子ノードが次のように呼び出されます。newChildren
配列内のすべての未処理の子ノードの最後の子ノードは、 new post; と呼ばれます。oldChildren
配列内のすべての未処理の子ノードの最初の子ノードは、次のように呼ばれます。oldChildren
配列内のすべての未処理の子ノードの最後の子ノードは、old post と呼ばれます。
さて、上記の概念を理解したので、それがどのように実装されるかを見てみましょう。
3. 新戦線対旧戦線
newChildren
配列内のすべての未処理の子ノードの最初oldChildren
の子ノードと、配列内のすべての未処理の子ノードの最初の子ノードを比較します。それらが同じであれば、それは素晴らしいことです。前の記事で説明した更新ノードに直接移動し、新旧ノードの位置が同じ場合はノードの移動操作は必要ありませんが、異なっていても問題ありませんので、次の 3 つのケースを試してください。
4. 新しい投稿と古い投稿
newChildren
配列内のすべての未処理の子ノードの最後のoldChildren
子ノードと、配列内のすべての未処理の子ノードの最後の子ノードを比較し、同じであれば、ノードを更新する操作を直接入力します。新しいノードと古いノードは同じであるため、one ノードの位置も同じなので、ノードを移動する必要はありません。異なる場合は、後で続けて試してください。
5. 新しい後と古い前線
newChildren
配列内のすべての未処理の子ノードの最後の子ノードと、配列内のすべての未処理の子ノードの最初の子ノードを比較しoldChildren
、それらが同じである場合は、ノードを更新する操作を直接入力し、配列を更新しますoldChildren
。ノードをnewChildren
配列内のノードと同じ位置に移動します。
このとき、ノードを移動する操作が表示されますが、ノードの移動で最も重要なのは、移動する場所を見つけることです。更新ノードはVNode
新しいノードに基づいて行われ、次に古いノードを操作して古いノードを新しいノードと同じにoldVNode
するoldVNode
VNode
必要があることを繰り返し強調してきました。現在の状況は、newChildren
配列内の最後の子ノードがoldChildren
配列内の最初の子ノードと同じである場合、oldChildren
図に示すように、最初の子ノードを配列内の最後の子ノードの位置に移動する必要があります。下:
この図から、配列内の未処理のノードをすべて移動した後で、oldChildren
配列内の最初の子ノードを移動する必要があることがわかります。
比較後、2 つのノードがまだ同じノードではないことが判明した場合は、最後のケースの試行を続けます。
6. 新しい前と古い後
newChildren
配列内のすべての未処理の子ノードの最初の子ノードと、oldChildren
配列内のすべての未処理の子ノードの最後の子ノードを比較します。それらが同じである場合は、ノードの更新操作に直接進み、次のノードを更新します。配列oldChildren
ノードをnewChildren
配列内のノードと同じ位置に移動します。
同様に、この場合のノードの位置移動のロジックは、「新しい後と古い前」のロジックに似ています。つまり、newChildren
配列内の最初の子ノードが配列oldChildren
内の最後の子ノードと同じである場合、次のようにする必要があります。oldChildren
以下の図に示すように、最後の子ノードが最初の子ノードの位置に移動します。
この図から、配列内のすべての未処理のノードの前に、oldChildren
配列内の最後の子ノードを移動する必要があることがわかります。
OK、上記はサブノードの比較と更新の最適化戦略の 4 つのケースです。上記の 4 つのケースを 1 つずつ試しても同じノードが見つからない場合は、前のループ方法を検索します。
7. ソースコードに戻ります
アイデアを分析してロジックを明確にした後、ソース コードに戻って、ソース コードに実装されているロジックが分析したものと同じかどうかを確認してみましょう。ソースコードは次のとおりです。
// 循环更新子节点
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // oldChildren开始索引
let oldEndIdx = oldCh.length - 1 // oldChildren结束索引
let oldStartVnode = oldCh[0] // oldChildren中所有未处理节点中的第一个
let oldEndVnode = oldCh[oldEndIdx] // oldChildren中所有未处理节点中的最后一个
let newStartIdx = 0 // newChildren开始索引
let newEndIdx = newCh.length - 1 // newChildren结束索引
let newStartVnode = newCh[0] // newChildren中所有未处理节点中的第一个
let newEndVnode = newCh[newEndIdx] // newChildren中所有未处理节点中的最后一个
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
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
// 以"新前"、"新后"、"旧前"、"旧后"的方式开始比对节点
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // 如果oldStartVnode不存在,则直接跳过,比对下一个
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 如果新前与旧前节点相同,就把两个节点进行patch更新
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 如果新后与旧后节点相同,就把两个节点进行patch更新
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
// 如果新后与旧前节点相同,先把两个节点进行patch更新,然后把旧前节点移动到oldChilren中所有未处理节点之后
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
// 如果新前与旧后节点相同,先把两个节点进行patch更新,然后把旧后节点移动到oldChilren中所有未处理节点之前
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 如果不属于以上四种情况,就进行常规的循环比对patch
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果在oldChildren里找不到当前循环的newChildren里的子节点
if (isUndef(idxInOld)) {
// New element
// 新增节点并插入到合适位置
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 如果在oldChildren里找到了当前循环的newChildren里的子节点
vnodeToMove = oldCh[idxInOld]
// 如果两个节点相同
if (sameVnode(vnodeToMove, newStartVnode)) {
// 调用patchVnode更新节点
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
// canmove表示是否需要移动节点,如果为true表示需要移动,则移动节点,如果为false则不用移动
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
/**
* 如果oldChildren比newChildren先循环完毕,
* 那么newChildren里面剩余的节点都是需要新增的节点,
* 把[newStartIdx, newEndIdx]之间的所有节点都插入到DOM中
*/
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
/**
* 如果newChildren比oldChildren先循环完毕,
* 那么oldChildren里面剩余的节点都是需要删除的节点,
* 把[oldStartIdx, oldEndIdx]之间的所有节点都删除
*/
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
ソースコードを読む前に、まず次のような概念があります。つまり、先ほど述べた最適化戦略では、ノードを前からまたは後ろから比較し、比較が成功した場合に更新プロセスが実行されるということです。つまり、最初のものを処理することも、最後のものを処理することもできるため、循環するときは、単に前から後ろ、または後ろから前に循環するだけではなく、両側から前に循環することになります。真ん中。
では、両側から中央までループするにはどうすればよいでしょうか? 以下の写真をご覧ください。
まず、4 つの変数を準備します。
- newStartIdx:
newChildren
配列内の開始位置の添字。 - newEndIdx:
newChildren
配列内の終了位置の添字。 - oldStartIdx:
oldChildren
配列内の開始位置の添字。 - oldEndIdx:
oldChildren
配列内の終了位置の添字。
ループすると、ノードを処理するたびに添字が図の矢印の方向に移動し、開始位置で示されるノードの処理が完了すると、1つ後ろに移動し、終了位置で示されるノードが処理されます。位置は処理後に 1 つ前に移動します。最適化戦略は古いノードと新しいノードをペアで更新することなので、1 回の更新で 2 つのノードが移動します。もっと率直に言うと、newStartIdx
後方oldStartIdx
にのみ移動でき (加算のみ)、newEndIdx
前方oldEndIdx
にのみ移動できます (減算のみ)。
開始位置が終了位置より大きい場合は、すべてのノードが通過されたことを意味します。
OK、この概念に基づいて、ソース コードを読み始めます。
-
oldStartVnode
存在しない場合は、直接スキップしてoldStartIdx
1 を追加し、次の値を比較します。// 以"新前"、"新后"、"旧前"、"旧后"的方式开始比对节点 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] } }
-
存在しない場合は
oldEndVnode
直接スキップされ、oldEndIdx
前のものと比較して 1 減算されます。else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] }
-
新しいフロント ノードが古いフロント ノードと同じである場合は、2 つのノードを更新し
patch
、同時に合計に 1 を加えoldStartIdx
、newStartIdx
位置を 1 つ戻します。else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] }
-
新しいノードが古いノードと同じである場合は、2 つのノードを更新し
patch
、合計を 1 減らしてoldEndIdx
、newEndIdx
1 つ前に移動します。else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] }
-
新しいノードが古いノードと同じである場合は、最初に 2 つのノードを更新し
patch
、次に古いノードをoldChilren
すべての未処理ノードの後ろに移動し、最後にoldStartIdx
1 を追加し、1 位置戻しnewEndIdx
、1 を減算し、1 位置前に移動します。else if (sameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] }
-
新しいフロント ノードが古いバック ノードと同じである場合は、最初に 2 つのノードを更新し
patch
、次に古いバック ノードをoldChilren
すべての未処理ノードの前に移動し、最後にnewStartIdx
1 を追加し、1 つ前の位置に戻しoldEndIdx
、1 を減算して前に進みます。 1つのポジションelse if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] }
-
上記4つの状況に当てはまらない場合は、通常のサイクル比較を行ってください。
patch
-
ループ内で
oldStartIdx
この値がより大きい場合oldEndIdx
、これはサイクルが以前oldChildren
よりも完了していることを意味しnewChildren
、newChildren
ループ内の残りのノードは追加する必要があるノードであり、[newStartIdx, newEndIdx]
間にあるすべてのノードがDOM
ループに挿入されます。if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) }
-
サイクル内で
newStartIdx
この値がより大きい場合、サイクルが以前よりも完了したnewEndIdx
ことを意味し、そのサイクル内の残りのノードが削除する必要があるノードとなり、その間にあるすべてのノードが削除されます。newChildren
oldChildren
oldChildren
[oldStartIdx, oldEndIdx]
else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) }
OK、処理が完了しました。ソースコード内の処理ロジックは、以前に解析したロジックと同じであることがわかります。
8. まとめ
この記事では、Vue
中性子ノード更新の最適化戦略を紹介しましたが、Vue
二重ループ内の大量のデータと時間の複雑さの増加によって引き起こされるパフォーマンスの問題を回避するために、4 つの特別な位置を比較することを選択したことがわかりました。はい、新しいフロントと古いフロント、新しいリアと古いリア、新しいリアと古いフロント、新しいフロントと古いリアです。それぞれのケースについて、グラフィックとテキストの形式でロジックを分析しました。最後に、ソース コードに戻り、ソース コードを読んで分析が正しいことを確認します。幸いなことに、以前の分析の各ステップのソース コードで対応する実装が見つかりました。これにより、分析が正しいことを確認できます。以上がVue
真ん中の処理patch
、つまりDOM-Diff
アルゴリズムの内容のすべてですが、この部分のソースコードをもう一度読んでいただくと、より明確に理解できると思います。