「ソースコード解析」仮想DOMアルゴリズムの実装方法

「ソースコード解析」仮想DOMアルゴリズムの実装方法

[[378869]]

前回の記事「仮想 DOM が実際の DOM に進化する方法」では、仮想 DOM ツリーを実際の DOM に変換してページにレンダリングする方法について説明しました。ただし、レンダリング プロセス中に、新しい仮想 DOM ツリーを実際の DOM に直接変換して、古い DOM 構造を置き換えます。実際の DOM の状態またはコンテンツが変更された場合、新しい仮想 DOM ツリーを再レンダリングして古いものを置き換えることは非常に無力になります。 DOM 構造全体の中の小さなデータや句読点だけを変更するシナリオ、あるいはデータ量が非常に大きい場合に、古い DOM 構造全体を置き換える必要があり、コンピューターのパフォーマンスが無駄になるシナリオを想像してみてください。

したがって、更新中に新しくレンダリングされた仮想 DOM ツリーと古い仮想 DOM ツリーを比較し、2 つのツリー間の違いを記録することが望まれます。記録される違いは、ページ上で実際の DOM 操作を実行し、それを実際の DOM 構造でレンダリングする必要があり、それに応じてページが変更される点です。これにより、ビュー全体の構造が最新の状態にレンダリングされたように見えますが、最終的に DOM 構造を操作すると、元の構造との差分のみが変更されるという効果が得られます。

つまり、仮想 DOM 差分アルゴリズムの主な考え方は次のとおりです。

1. 仮想 DOM 構造を実際の DOM 構造に変換し、古い DOM (最初は古いものは未定義) に置き換えて、ページにレンダリングします。

2. 状態が変化すると、新しい仮想 DOM ツリーがレンダリングされ、古い仮想 DOM ツリーと比較され、比較後に差異が記録されます。

3. 最終的な差異部分は実際の DOM 構造に変換され、ページ上にレンダリングされます。

成し遂げる

古い仮想ノードと新しい仮想ノードの比較中に、次の状況が発生する可能性があります。Vue を例に、Vue2.0 で Diff アルゴリズムがどのように実装されているかを見てみましょう。

2つの要素のラベルを比較する

タグが異なる場合は、直接置き換えてください。例: div は p になります。

  1. div->p
  2.  
  3. <<<<<<<<ヘッド
  4. <p>フロントエンドブリーフィング</p>
  5.  
  6. =========
  7.  
  8. <div>フロントエンドの説明</div>
  9. >>>>>>>>

仮想ノードのタグ属性が等しいかどうかを判断します。等しくない場合は、新しい仮想 DOM ツリーを実際の DOM 構造に変換し、元のノードを置き換えます。

  1. (oldVnode.tag != vnode.tag) の場合 {
  2. oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el)を返します
  3. }

効果画像:


2つの要素のテキストを比較します

タグが同じ場合は、テキストが同じかどうかを比較します。テキストが異なる場合は、テキストの内容を置き換えるだけです。

  1. <<<<<<<<ヘッド
  2. <div>フロントエンド</div>
  3. =========
  4. <div>ニュースレター</div>
  5. >>>>>>>>

両ノードのタグはdivなので、子の仮想DOMツリーが同じかどうかを比較します。子タグは未定義なので、テキストノードであることを意味します。この時点で、テキストコンテンツが一貫しているかどうかを比較できます。

  1. if (!oldVnode.tag) {
  2. //テキスト比較
  3. (oldVnode.text != vnode.text) の場合 {
  4. 戻り値(oldVnode.el.textContent = vnode.text);
  5. }
  6. }

効果画像:


タグ属性の比較

2 つのタグが同じ場合は、タグの属性を比較します。属性が更新されると、新しい属性と古い属性を比較することで、次の状況が発生する可能性があります。

1. 属性の比較

古い仮想ノードに属性があり、新しい仮想ノードにない場合は、古い仮想ノードの属性を削除する必要があります。

  1. let newProps = vnode.data || {}; //新しいプロパティ
  2. el = vnode.el とします。
  3. // 古いものにはありますが、新しいものにはありません。属性を削除する必要があります。
  4. for (letキー  oldProps){
  5. if (!newProps[キー]) {
  6. el.removeAttribute( key ); //実際のDOM属性を削除します
  7. }
  8. }

逆に、古い仮想ノードには属性がなく、新しい仮想ノードには属性がある場合は、新しい属性を設定するだけです。

  1. // 新しいものがある場合は、新しいものに更新するだけです
  2. for (letキー  newProps){
  3. el.setAttribute(キー, newProps[キー] );
  4. }
  • 対応するソースコード アドレス: src\platforms\web\runtime\modules\attrs.js

2. スタイル処理

新しいスタイルが古いスタイル内に存在する場合は、古いスタイルを削除します。

  1. - スタイル={色:赤}
  2. + スタイル = {背景:赤}

  1. newStyle を newProps.style とします || {};
  2. oldStyle を oldProps.style とします || {};
  3. // 古いスタイルにはありますが、新しいスタイルにはありません。古いスタイルを削除します。
  4. for (letキー スタイル){
  5. if (!newStyle[キー]) {
  6. el.style[キー] = "" ;
  7. }
  8. }

逆に、古いスタイルが存在せず、新しいスタイルが存在する場合は、新しいスタイルを直接更新するだけです。

  1. for (letキー  newProps){
  2. if (キー== "スタイル " ) {
  3. for (let styleName in newProps.style) {
  4. el.style[スタイル名] = newProps.style[スタイル名];
  5. }
  6. }
  7. }
  • 対応するソースコード アドレス: src\platforms\web\runtime\modules\style.js

3. クラス名の処理

クラス名の処理には、新しいノードのクラス名を使用します。

  1. - クラス = "タイトル ant-title"  
  2. + クラス = "タイトル ant-mian-title"  

  1. for (letキー  newProps){
  2. if (キー== "クラス " ) {
  3. el.className = newProps.class;
  4. }
  • 対応するソースコードアドレスは src\platforms\web\runtime\modules\class.js です。

息子の比較

息子を比較する過程では、次のような状況に分けられます。

1. 古いノードには息子がいますが、新しいノードには息子がいません。古いノードの息子を削除するだけです。

  1. if (isDef(oldCh)) {
  2. Vnodesを削除します(oldCh, 0, oldCh.length - 1)
  3. }
  4. =========================================
  5. (oldChildren.length > 0) の場合 {
  6. el.innerHTML = "" ;
  7. }

2. 古いノードには子がなく、新しいノードには子があります。子を走査して実際の DOM 構造に変換し、ページに追加します。

  1. if (isDef(ch)) {
  2. if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '' )
  3. addVnodes(elm, null , ch, 0, ch.length - 1, 挿入されたVnodeQueue)
  4. }
  5. ===============================================================
  6. (newChildren.length > 0) の場合 {
  7. ( i = 0 とします; i < newChildren.length; i++) {
  8. 子をnewChildren[i]とします。
  9. el.appendChild(Elm(child) を作成します);
  10. }
  11. }

3. 古いノードには息子がおり、新しいノードにも息子がいる

古いノードの子と新しいノードの子の両方が存在し、それらが等しくない場合、この状況はさらに複雑になり、diff アルゴリズムの中核にもなります。

vue2.0 では、古いノードと新しいノードを比較するためにダブルポインター方式が使用されています。古いノードと新しいノードは同時に同じ方向にループされます。1 つのノードのループが完了すると、ループは終了します。古いノードが先に終了した場合は、新しいノードの残りの要素をレンダリング リストに追加します。新しいノードが先に終了した場合は、古いノードの残りの要素を削除します。

古いノードの開始位置と終了位置、および新しいノードの開始位置と終了位置を含む開始ポインターを定義します。

  1. let oldStartIndex = 0; //古いインデックス
  2. let oldStartVnode = oldChildren[0]; //古いインデックスが指すノード
  3. oldEndIndex = oldChildren.length - 1 とします。
  4. oldEndVnode = oldChildren[oldEndIndex]とします。
  5.  
  6. let newStartIndex = 0; //新しいインデックス
  7. let newStartVnode = newChildren[0]; //新しいインデックスが指すノード
  8. newEndIndex = newChildren.length - 1 とします。
  9. newEndVnode = newChildren[newEndIndex]とします。

2つのノードのキーとタグが等しいかどうかを判断して同じ要素を判別する

  1. 関数sameVnode(a, b) {
  2. 戻る
  3. a.キー=== b.キー&& (
  4. a.タグ === b.タグ &&
  5. ...
  6. ) || (
  7. ...
  8. }

肯定的な順序で並べる

追加ノードが右側にある場合は、左から右に、古い開始ノードと新しい開始ノードが同じノードであるかどうかを判断します。同じノードである場合は、patchVode メソッドを呼び出して子ノードを再帰的に返し、古いノードと新しいノードの添字に 1 を追加して、添字が子の長さよりも大きくなるまで右に移動します。


  1. 同じVnodeの場合(古い開始Vnode、新しい開始Vnode)) {
  2. パッチVnode(古い開始Vnode、新しい開始Vnode、挿入されたVnodeQueue、新しいCh、新しい開始Idx)
  3. 古い開始Vノード = 古いCh[++古い開始Idx]
  4. 新しい開始Vノード = 新しいCh[++新しい開始Idx]
  5. }

効果画像:


上図のように、レンダリング ビューに新しいノードが過剰に追加された場合、左から右に比較すると、g ノードの次の el は null になり、insertBefore は appendChild メソッドを使用して後方に挿入することと同等になります。右から左の場合、g ノードの次の el は a になり、insertBefore を使用することは a の前にノードを挿入することと同等になります。

  1. if (古い開始インデックス > 古い終了インデックス) {
  2. (i = newStartIndex; i <= newEndIndex; i++)の場合{
  3. ele = とする
  4. 新しい子[新しい終了インデックス + 1] == null  
  5. ?ヌル 
  6. : newChildren[newEndIndex + 1].el;
  7. 親要素をcreateElm(newChildren[i]), eleの前に挿入します。
  8. }
  9. }

古いノードが冗長である場合、これらのノードは不要であり、削除できることを意味します。削除プロセス中に null が表示される場合、ノードは処理済みであり、スキップできることを意味します。

  1. (新しい開始ID > 新しい終了ID) の場合 {
  2. (i = oldStartIndex; i <= oldEndIndex; i++)の場合{
  3. 子を oldChildren[i] とします。
  4. if (child!= undefined) {
  5. 親要素を削除します。
  6. }
  7. }
  8. }

追加ノードが左側にある場合、新しいノードと古いノードの終了ノードから始まって、添え字が 1 ずつ減ります。

  1. 同じVnodeの場合(古いEndVnode、新しいEndVnode) {
  2. パッチVnode(古い終了Vnode、新しい終了Vnode、挿入されたVnodeQueue、新しいCh、新しい終了Idx)
  3. oldEndVnode = oldCh[ --oldEndIdx]  
  4. newEndVnode = newCh[ --newEndIdx]  
  5. }

順序を逆にする

新しいノードと古いノードが逆になっている場合は、古いノードの開始ノードを新しいノードの終了ノードと比較するか、古いノードと終了ノードを新しいノードの開始ノードと比較します。


古いノードの開始ノードと新しいノードの終了ノードが同じノードである場合、古い終了ノードの次のノードの前に古い開始ノードを挿入し、ノードの対応する添字をそれぞれ右と左に移動し、対応する値を取得してトラバーサルを続行します。

  1. if (sameVnode(oldStartVnode, newEndVnode)) { // Vnodeが右に移動しました 
  2. パッチVnode(古い開始Vnode、新しい終了Vnode、挿入されたVnodeQueue、新しいCh、新しい終了Idx)
  3. canMove && nodeOps.insertBefore(親Elm、oldStartVnode.elm、nodeOps.nextSibling(oldEndVnode.elm))
  4. 古い開始Vノード = 古いCh[++古い開始Idx]
  5. newEndVnode = newCh[ --newEndIdx]  
  6. }

古いノードの終了ノードと新しいノードの開始ノードが同じノードである場合、古いノードの終了ノードを古いノードの開始ノードの前に挿入し、ノードの対応する添字をそれぞれ左と右に移動し、対応する値を取得してトラバーサルを続行します。

  1. if (sameVnode(oldEndVnode, newStartVnode)) { // Vnodeが左に移動しました 
  2. パッチVnode(古い終了Vnode、新しい開始Vnode、挿入されたVnodeQueue、新しいCh、新しい開始Idx)
  3. canMove && nodeOps.insertBefore(親Elm、古い終了Vnode.elm、古い開始Vnode.elm)
  4. oldEndVnode = oldCh[ --oldEndIdx]  
  5. 新しい開始Vノード = 新しいCh[++新しい開始Idx]
  6. }

関係なし

比較処理中に子ノード間に関係がない場合は、新しいノードの開始ノードから始めて、古いノードのすべてのノードと順番に比較します。同じものがない場合は、新しいノードを作成し、古いノードの開始ノードの前に挿入します。ループ中に同じ要素が見つかった場合は、古い要素を直接再利用し、新しいノードと同じ古いノードを古いノードの開始ノードの前に挿入します。配列の崩壊を防ぐために、削除された古いノードの位置を未定義に設定し、最後に冗長な古いノードをすべて削除します。


キャッシュ グループを設定して、古いノードのキーとインデックスを使用してマッピング テーブルを作成します。新しいノードのキーは、古いマッピング テーブルでフィルタリングされます。見つからない場合は再利用されず、新しいノードが作成され、古いノードの開始ノードの前に挿入されます。

  1. 関数createKeyToOldIdx(子){
  2. let i、キー 
  3. 定数マップ = {}
  4. children.forEach((item, index ) => {
  5. if (isDef( item.key )) {
  6. map[ item.key ] = index ; //{a:0,b:1,c:2,d:3,e:4,f:5,g:6}
  7. }
  8. 戻る地図
  9. }

古いノードで見つかった場合は、古いノードを古いノードの開始ノードの前に移動

  1. マップをcreateKeyToOldIdx(oldChildren)にします。
  2. //息子たちの間には関係はない
  3. let moveIndex = map[newStartVnode. key ]; //最初の仮想ノードのキーを取得し、古いものを検索します
  4.  
  5. 移動インデックス == 未定義の場合{
  6. 親.insertBefore(createElm(newStartVnode),oldStartVnode.el);
  7. }それ以外{
  8. let moveVNode = oldChildren[moveIndex]; //この古い仮想ノードを移動する必要がある
  9. oldChildren[moveIndex] = null ;
  10. 親.insertBefore(moveVNode.el、oldStartVnode.el);
  11. patch(moveVNode,newStartVnode) //プロパティをsonと比較する
  12. }
  13. newStartVnode = newChildren[++newStartIndex] //新しいものを使用して古いものを検索し続けます

移動処理中、開始ポインターと終了ポインターが null を指している可能性があります。これらが null を指している場合は比較が行えず、ポインターをスキップして次の要素を指すことができます。

  1. if (isUndef(oldStartVnode)) {
  2. oldStartVnode = oldCh[++oldStartIdx] // Vnodeが左に移動されました 
  3. }そうでない場合 (isUndef(oldEndVnode)) {
  4. oldEndVnode = oldCh[ --oldEndIdx]  
  5. }

ソースコードアドレス: src/core/vdom/patch.js

なぜキーを使用するのですか?

私は醜いので多くは語りません、まずは写真を見てみましょう


キー付き


キーなし

上図に示すように、最初の図はキーがある場合、2 番目の図はキーがない場合を示しています。表示されるコンテンツにキーがある場合、キー A、B、C、D を持つ 4 つのノードが再利用されることが明確にわかります。その結果、新しく作成された E ノードが C ノードの前に挿入されるだけで、レンダリングが完了します。キーがない場合、3 つのノード E、C、D が作成され、再利用率が低下し、パフォーマンスはキーがある場合ほど高くなりません。

なぜインデックスをキーとして使用できないのですか?

通常の開発プロセスでは、ページの静的レンダリングのみを使用する場合は、インデックスをキーとして使用できます。ページに複雑な論理的な変更がある場合、インデックスをキーとして使用することは、キーがないのと同じです。

  1. <li index =0>あ</li> <li index =0>こ</li>
  2. <li index =1>B</li> <li index =1>B</li>
  3. <li index =2>C</li> <li index =2>A</li>

上記のコードに示すように、添え字 0 と 2 で A と C の位置を変更した後、ノード A と C を再作成する必要があります。このとき、C の添え字は 0 で、A の添え字は 2 です。 id または一意の識別子をキーとして使用することは、要素 A と C の位置をシフトすることと同じです。翻訳のパフォーマンスはノード作成のパフォーマンスよりも高くなります。

インデックスをキーとして使用すると、予期しない問題が発生する可能性があります。ノード B を削除すると、初期値は B ですが、C になります。

要約する

Vue2.0 の diff アルゴリズム pathVode メソッドの基本的な考え方は、次のようにまとめることができます。

1. oldVode と newVode が同じオブジェクトであるかどうかを判断します。同じ場合は、直接戻ります。 2. 実際の DOM を el として定義します。

3. oldVode と newVode の両方にテキスト ノードがあり、それらが等しくない場合は、oldVode のテキスト ノードを newVode のテキスト ノードに設定します。

4. oldVode に子ノードがあり、newVode にない場合は、子ノードを削除します。

5. oldVode に子ノードがない場合、newVode には子ノードがあります。次に、子ノードを実際の DOM に変換し、el に追加します。 6. 両方に子ノードがある場合は、updateChildren関数を実行して子ノードを比較します。

上記は、Vue レンダリング プロセス全体のパフォーマンスに重要な役割を果たす Diff アルゴリズムのプロセス全体です。

<<:  「アルゴリズムとデータ構造」JavaScript のリンク リスト

>>:  AIは主人の命令に従わず、主人を笑いさえしました!意識が目覚めた?

ブログ    

推薦する

人工知能は石油・ガス生産者の業務改善と温室効果ガス排出削減に貢献

[[437362]]石油・ガス生産者の操業実績を測る指標は数多くあり、効率性の向上、コストの削減、油...

「天機」が本日ネイチャー誌の表紙を飾る:清華大学のShi Luping氏のチームが世界初の異種融合脳型チップをリリース!

清華大学は、世界初の異種融合脳型コンピューティングチップ「天機チップ」を開発しました。このチップで駆...

企業向けの優れたビジネス インテリジェンス ツール 10 選

規模に関係なく、企業はニーズに合わせてカスタマイズされたビジネス インテリジェンス ツールを使用して...

AI人材の確保をめぐる秘密の戦い:中国が勝利する可能性は?

[[251811]]画像ソース @Visual China人工知能の概念は、提唱されてから60年以...

大学における人工知能への熱意を「クール」に振り返る

大学は関連専攻を開設する際に、教授委員会と学術委員会を組織し、国の人材政策、業界の人材需要、国内外の...

AIはソフトウェアテスターの仕事を「奪う」のでしょうか?

この記事は公開アカウント「Reading Core Technique」(ID: AI_Discov...

過大評価されすぎた人工知能バブルは、どのように崩壊するのでしょうか。

実は、似たような事件は以前にも起きている。江蘇省衛星テレビの番組「The Brain」では、百度脳が...

ソラがビデオ世代を爆発させたとき、Metaは中国の著者の主導で、エージェントを使用してビデオを自動的に編集し始めました。

最近、AIビデオの分野は非常に活発になっており、OpenAIが立ち上げた大規模なビデオ生成モデルであ...

モデルの再現が難しいのは必ずしも作者のせいではない。研究により、モデルの構造に問題があることが判明した。

この記事はAI新メディアQuantum Bit(公開アカウントID:QbitAI)より許可を得て転載...

2017 年に最も価値のある機械学習のスキルや知識は何ですか?

2017 年に最も価値のある機械学習スキルはどれでしょうか? Quora の 2 つの回答では、最...

2021 年の人工知能の 4 つのビジネス アプリケーション

[[409268]] [51CTO.com クイック翻訳]人工知能は万能の機械として描かれることが多...

低品質の AIGC コンテンツがインターネット エコシステムに溢れかえれば、エコシステムは破壊されてしまいます。

少し前、ChatGPT は突然人気を博し、ユーザーベースが急速に増加しました。多くの人が「生成 AI...

...

...