この記事では、Vue の仮想 dom 比較原理について説明します (例を示して説明します)。必要な方は参考にしていただければ幸いです。
まず、なぜ仮想 dom 比較ステージがあるのかについて話しましょう。Vue がデータ駆動型ビューであることはわかっていますが (データの変更によりビューも変更されます)、特定のデータが変更されるとそのことがわかります。 、ビューは部分的です。データに対応するビューを正確に見つけて、全体を再レンダリングするのではなく更新することで更新するにはどうすればよいですか?次に、データ変更前後の DOM 構造を取得し、相違点を見つけて更新する必要があります。
仮想 dom は本質的に、実際の dom から抽出された 単純なオブジェクトです。単純な p には 200 を超える属性が含まれていますが、実際に必要なのは tagName
だけである可能性があるのと同様に、実際の dom に対する直接操作はパフォーマンスに大きく影響します。
簡略化された仮想ノード (vnode) には、大まかに次の属性が含まれます:
{ tag: 'p', // 标签名 data: {}, // 属性数据,包括class、style、event、props、attrs等 children: [], // 子节点数组,也是vnode结构 text: undefined, // 文本 elm: undefined, // 真实dom key: undefined // 节点标识 }
仮想 dom の比較は、新しいノードを見つけることです。 (vnode ) と古いノード (oldVnode) を比較し、相違点にパッチを当てます。一般的なプロセスは次のとおりです
#古いノードと新しいノードが類似していない場合は、新しいノードに基づいて DOM を直接作成します。が類似している場合は、まずクラス、スタイル、イベント、プロパティ、属性などのデータを比較し、相違がある場合は、対応する更新関数を呼び出してから、子ノードの比較に を使用します。 diff アルゴリズム 、これがこの記事の焦点であり、難しさです。
Children Compare プロセス中に、同様の
childVnode が見つかった場合、
再帰的にパッチ適用ウィンドウが表示されることに注意してください。プロセス。 ソースコード解析
Start
関数を見てみましょう: <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">function patch (oldVnode, vnode) {
var elm, parent;
if (sameVnode(oldVnode, vnode)) {
// 相似就去打补丁(增删改)
patchVnode(oldVnode, vnode);
} else {
// 不相似就整个覆盖
elm = oldVnode.elm;
parent = api.parentNode(elm);
createElm(vnode);
if (parent !== null) {
api.insertBefore(parent, vnode.elm, api.nextSibling(elm));
removeVnodes(parent, [oldVnode], 0, 0);
}
}
return vnode.elm;
}</pre><div class="contentsignin">ログイン後にコピー</div></div>
この関数は、古い vnode と新しい vnode の 2 つのパラメーターを受け取ります。渡される 2 つのパラメータには大きな違いがあります。違い: oldVnode の elm
は実際の dom を指しますが、vnode の elm
は未定義です...ただし、patch の後()
メソッド、vnode の elm
もこの (更新された) 実 dom を指します。 古い vnode と新しい vnode が類似しているかどうかを判断する
メソッドは非常に簡単で、tag
と key## かどうかを比較することです。 # 一貫性があります。
function sameVnode (a, b) { return a.key === b.key && a.tag === b.tag; }
新旧の vnode の一貫した処理については、前によく言及したパッチ適用です。パッチ当てとは具体的に何ですか? patchVnode()
メソッドを見てください:
function patchVnode (oldVnode, vnode) { // 新节点引用旧节点的dom let elm = vnode.elm = oldVnode.elm; const oldCh = oldVnode.children; const ch = vnode.children; // 调用update钩子 if (vnode.data) { updateAttrs(oldVnode, vnode); updateClass(oldVnode, vnode); updateEventListeners(oldVnode, vnode); updateProps(oldVnode, vnode); updateStyle(oldVnode, vnode); } // 判断是否为文本节点 if (vnode.text == undefined) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue) } else if (isDef(ch)) { if (isDef(oldVnode.text)) api.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { api.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { api.setTextContent(elm, vnode.text) } }
パッチは実際にさまざまな updateXXX() 関数を呼び出して、実際の dom のさまざまな属性を更新しています。各更新関数は似ています。例として updateAttrs()
を取り上げます。
function updateAttrs (oldVnode, vnode) { let key, cur, old const elm = vnode.elm const oldAttrs = oldVnode.data.attrs || {} const attrs = vnode.data.attrs || {} // 更新/添加属性 for (key in attrs) { cur = attrs[key] old = oldAttrs[key] if (old !== cur) { if (booleanAttrsDict[key] && cur == null) { elm.removeAttribute(key) } else { elm.setAttribute(key, cur) } } } // 删除新节点不存在的属性 for (key in oldAttrs) { if (!(key in attrs)) { elm.removeAttribute(key) } } }
属性 (Attribute
) の更新関数の一般的な考え方は次のとおりです。
vnode 属性をトラバースします。oldVnode と異なる場合は、setAttribute()
を呼び出して変更します。 oldVnode 属性 (同じでない場合)
ブール属性辞書にあるかどうかを判断する
['allowfullscreen'、'async'、'autofocus'、'autoplay'、'checked'、'compact'、'controls'、'declare'、...]例:
すべてのデータを比較したら、子ノードを比較します。まず、現在の vnode がテキスト ノードであるかどうかを判断します。テキスト ノードの場合は、子ノードの比較を考慮する必要はありません。要素ノードの場合は、次の 3 つの状況で考慮する必要があります。 ## 古いノードと新しいノードの両方に子があり、子ノードの比較を入力します (差分アルゴリズム)。
新しいノードには子がありませんが、古いノードには子があるため、ループ内で dom ノードを削除します。##新しいノードには子がありますが、古いノードには子がありません。ノードに子がない場合は、ループ内で dom ノードを作成します。
子ノードの比較
を直接分析します。oldCh
と newCh
は、それぞれ古い子ノード配列と新しい子ノード配列を表し、独自の先頭ポインターと末尾ポインター oldStartIdx
、 を持ちます。 oldEndIdx
、newStartIdx
、newEndIdx
、vnode は配列に格納されており、理解しやすいように、異なるものを表す a、b、c、d などに置き換えられます。タグのタイプ (p、span、p) vnode オブジェクト。
子ノードの比較は、基本的に、先頭ノードと末尾ノードのループ比較です。ループの終了の兆候は、古い子ノード配列または新しい子ノード配列が走査されたことです (つまり、oldStartIdx > oldEndIdx || newStartIdx > newEndIdx
)。 サイクル プロセス を大まかに見てみましょう:
最初のステップ 相互比較 。それらが類似している場合、古いヘッド ポインターと新しいヘッド ポインターは後方に移動し (つまり、oldStartIdx
&& newStartIdx
)、実際の DOM は変更されず、類似していない場合は次のサイクルに入ります。 、2番目のステップに入ります。
2 番目のステップ 末尾同士を比較します。それらが類似している場合、古い終了ポインタと新しい終了ポインタは前方に移動され (つまり、oldEndIdx--
&& newEndIdx--
)、実際の DOM は変更されず、次のサイクルに入ります。 ; 類似していない場合は、3 番目のステップに進みます。
3 番目のステップ 頭と尾を比較します。それらが類似している場合、古いヘッド ポインタは後方に移動し、新しいテール ポインタは前方に移動します (つまり、oldStartIdx
&& newEndIdx--
)。未確認の dom シーケンスのヘッドは、終了して次のサイクルに入ります。同様ではありません。ステップ 4 に進みます。
ステップ 4 尻尾と頭を比較します。それらが類似している場合、古い末尾ポインタは前方に移動され、新しい先頭ポインタは後方に移動されます (つまり、oldEndIdx--
&& newStartIdx
)。未確認の dom シーケンスの末尾が移動されます。同様ではなく、ステップ 5 に進みます。
5 番目のステップは、ノードにキーがあり、同じ Vnode が古い子ノード配列で見つかった場合 (タグとキーの両方が一致している)、その dom を先頭に移動することです。現在の実際の dom シーケンスの新しいヘッド ポインターが後方に移動します (つまり、newStartIdx
)。それ以外の場合は、vnode (vnode[newStartIdx].elm
) に対応する dom が挿入されます。現在の実際の dom シーケンスの先頭に移動し、新しい先頭ポインタは後方に移動します (つまり、newStartIdx
)。
まずはキーなしの状況を見て、よりわかりやすくアニメーションを付けてみましょう。
# この図を読めば、diff アルゴリズムの本質がよりよく理解できると思います。プロセス全体は比較的単純です。上の図では、各状況を含む合計 6 つのサイクルが入力されています。それらを 1 つずつ説明しましょう:
初回はすべて似ています (両方とも です)。 a
)、dom は変化せず、新旧両方のヘッド ポインタが後方に移動します。 a
ノードが確認された後の実際の dom シーケンスは a,b,c,d,e,f
、未確認の dom シーケンスは b,c,d, e,f
;
2 回目は尾と尾が似ています (どちらも f
)、dom は変化せず、古い尾と新しい尾ポインタが前に移動します。 f
ノードが確認された後の実際の dom シーケンスは a,b,c,d,e,f
、未確認の dom シーケンスは b,c,d, e
;
3 回目は、先頭と末尾が似ています (両方とも b
)。現在残っている実際の dom シーケンスの先頭が に移動されます。最後に、古いヘッド ポインタが後方に移動し、新しいヘッド ポインタが後方に移動し、テール ポインタが前方に移動します。 b
ノードが確認された後の実際の dom シーケンスは a,c,d,e,b,f
、未確認の dom シーケンスは c,d,e## になります。 #;
e)。現在残っている実際の dom シーケンスの末尾が先頭に移動されます。 、古いテールポインタは前方に移動し、新しいヘッドポインタはシフトの後ろに移動します。
eノードが確認された後の実際の dom シーケンスは
a,e,c,d,b,f となり、未確認の dom シーケンスは
c,d になります。
gノードが挿入された後の実際の dom シーケンスは
a,e,g,c,d,b,f、未確認の dom シーケンスは
c,d## になります。 #;
ノードが挿入された後の実際の dom シーケンスは a,e,g,h,c,d,b,f
となり、未確認の dom シーケンスは になります。 c,d
;
)。次に、冗長な古い dom (oldStartIdx -> oldEndIdx
) をすべて削除する必要があります。上記の例では、c,d
; です。 ##new バイト ポイント配列 (oldCh) が走査されました (oldStartIdx > oldEndIdx
)。次に、追加の新しい dom (
上面说了这么多都是没有key的情况,说添加了:key
可以优化v-for
的性能,到底是怎么回事呢?因为v-for
大部分情况下生成的都是相同tag
的标签,如果没有key标识,那么相当于每次头头比较都能成功。你想想如果你往v-for
绑定的数组头部push数据,那么整个dom将全部刷新一遍(如果数组每项内容都不一样),那加了key
会有什么帮助呢?这边引用一张图:
有key
的情况,其实就是多了一步匹配查找的过程。也就是上面循环流程中的第五步,会尝试去旧子节点数组中找到与当前新子节点相似的节点,减少dom的操作!
有兴趣的可以看看代码:
function updateChildren (parentElm, oldCh, newCh) { 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, elmToMove, before while (oldStartIdx oldEndIdx) { before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
以上がVueにおける仮想dom比較の原理の紹介(サンプル解説)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。