Vueのdiffアルゴリズムについての簡単な説明

Vueのdiffアルゴリズムについての簡単な説明

概要

diff アルゴリズムは Vue のコアコンテンツと言えます。私はこれまで開発で Vue を使用したことがあり、具体的なコアコンテンツについてはあまり知りませんでした。最近、この分野のコンテンツを偶然読みました。Vue2.0 の diff アルゴリズムの実装についてお話ししましょう。具体的には、実装されているいくつかの機能から分析していきます。

仮想ドム

仮想 DOM は実際の DOM のデータを抽出し、オブジェクトの形式でツリー構造をシミュレートします。

例えば、以下は実際のDOMです。

<div>
   <p>1234</p>
   <div>
       <span>1111</span>
   </div>
</div>

実際のDOMに従って生成された仮想DOMは次のようになります

 var Vnode = {
     タグ: 'div',
     子供たち: [
         {
             タグ: 'p',
             テキスト: '1234'
         },
         {
             タグ: 'div',
             子供たち:[
                 {
                     タグ: 'span',
                     テキスト: '1111'
                 }
             ]
         }
     ]
 }

原理

diff の原理は、現在の実際の DOM が仮想 DOM を生成することです。仮想 DOM 内のノードのデータが変更されると、新しい Vnode が生成されます。次に、この Vnode が古い oldVnode と比較されます。違いがある場合は、実際の DOM 上で直接変更されます。

実装プロセス

diff アルゴリズムの実装プロセスの中核は patch です。patchVnode、sameVnode、updateChildren メソッドは注目に値します。以下、順に説明します。

パッチ方式

パッチのコアロジックは、2つのVnodeノードを比較し、その違いをビューに更新することです。比較方法は、各レベルをループするのではなく、ピア比較です。比較後に違いが見つかった場合、これらの違いはビューに更新されます。比較方法の例は次のとおりです。

sameVnode関数

sameVnode の機能は、2 つのノードが同じかどうかを判断することです。判断の基準は、キー値、タグ、isCommit、入力タイプが一貫しているかどうかなどです。この方法にはいくつかの欠陥があります。v-for の下のキー値がインデックスを使用する状況に直面した場合、再利用可能なノードとして判断される可能性もあります。

インデックスをキー値として使用しないことをお勧めします。

patchVnode関数

// いくつかのパラメータを渡します。oldVnode は古いノード、vnode は新しいノード、readOnly は読み取り専用ノードかどうかを表します。関数 patchVnode (
    古いVノード、
    vノード、
    挿入されたVnodeQueue、
    所有者配列、
    索引、
    削除のみ
  ){
    if (oldVnode === vnode) { //古いノードと新しいノードが一致している場合は比較は不要です。 return return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {   
      // 再利用された vnode を複製する
      vnode = ownerArray[インデックス] = cloneVNode(vnode)
    }

    const elm = vnode.elm = 古いVnode.elm

    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      (vnode.asyncFactory.resolved がisDefである)場合
        水和物(古いVnode.elm、vnode、挿入されたVnodeQueue)
      } それ以外 {
        vnode.isAsyncPlaceholder = true
      }
      戻る
    }

    // 静的ツリーの要素を再利用します // vnode が複製された場合にのみこれを実行します // 新しいノードが複製されていない場合は、レンダリング関数が複製されたことを意味します // hot-reload-api を介してリセットし、適切な再レンダリングを行う必要があります。
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === 古いVnode.key &&
      (vnode.isCloned が True の場合 || vnode.isOnce が True の場合)
    ){
      vnode.componentInstance = 古いVnode.componentInstance
      戻る
    }

    私は
    定数データ = vnode.data
    isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch) の場合
      i(古いVノード、vノード)
    }

    定数 oldCh = oldVnode.children
    定数ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      (i = 0; i < cbs.update.length; ++i) の場合、cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    if (isUndef(vnode.text)) {
      isDef(oldCh) と isDef(ch) のどちらが正しいか
        oldCh !== ch の場合、elm の子を更新します。oldCh の子は ch の子であり、insertedVnodeQueue の子は ch の子であり、removeOnly の子は elm の子です。
      } そうでなければ (isDef(ch)) {
        process.env.NODE_ENV !== 'production' の場合 {
          重複キーのチェック(ch)
        }
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, 挿入されたVnodeQueue)
      } そうでなければ (isDef(oldCh)) {
        Vnodesを削除します(oldCh, 0, oldCh.length - 1)
      } そうでない場合 (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }
    } そうでない場合 (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm、vnode.text) は、
    }
    if (isDef(データ)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

具体的な実装ロジックは次のとおりです。

  1. 古いノードと新しいノードが同じ場合は、変更は必要なく、ノードが直接返されます。
  2. 古いノードと新しいノードの両方が静的ノードであり、同じキーを持っている場合、vnode がクローンされたノードまたは v-once ディレクティブによって制御されるノードであるときは、oldVnode.elm と oldVnode.child を vnode にコピーするだけです。
  3. vnodeがコメントノードかテキストノードかを判断し、次の処理を実行する。
    1. vnode がテキスト ノードまたはコメント ノードの場合、vnode.text!== oldVnode.text のときは、vnode のテキスト コンテンツのみを更新する必要があります。
    2. oldVnodeとvndoeはどちらも子ノードを持っています。子ノードが異なる場合はupdateChildrenメソッドが呼び出されます。具体的な実装は以下のとおりです。
    3. vnode のみに子ノードがある場合は、環境を判別します。実稼働環境でない場合は、checkDuplicateKeys メソッドを呼び出して、キー値が重複していないかどうかを判別します。次に現在のchをoldVnodeに追加します
    4. oldVnodeのみに子ノードがある場合は、現在のノードを削除するメソッドを呼び出します。

updateChildren 関数

updateChildren は、その名前が示すように、子ノードを更新するためのメソッドです。上記の patchVnode メソッドから、新しいノードと古いノードの両方に子ノードがある場合にこのメソッドが実行されることがわかります。実装ロジックを見てみましょう。これに似た例の図もいくつかあるかもしれません。まずはコードを見てみましょう。

関数 updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    oldStartIdx = 0 とする
    newStartIdx = 0 とします
    oldEndIdx = oldCh.length - 1 とします。
    oldStartVnode = oldCh[0]とする
    oldEndVnode = oldCh[oldEndIdx]とする
    newEndIdx = newCh.length - 1 とします。
    newStartVnode = newCh[0]とする
    newEndVnode = newCh[newEndIdx]とします。
    oldKeyToIdx、idxInOld、vnodeToMove、refElm を使用します。

    // removeOnly は <transition-group> でのみ使用される特別なフラグです
    // 削除された要素が正しい相対位置に留まるようにする
    // 退出遷移中
    const canMove = !removeOnly

    process.env.NODE_ENV !== 'production' の場合 {
      重複キーのチェック(newCh)
    }

    (古い開始Idx <= 古い終了Idx && 新しい開始Idx <= 新しい終了Idx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnodeが左に移動されました
      } そうでない場合 (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } そうでない場合 (sameVnode(oldStartVnode, newStartVnode)) {
        パッチVnode(古い開始Vnode、新しい開始Vnode、挿入されたVnodeQueue、新しいCh、新しい開始Idx)
        古い開始Vノード = 古いCh[++古い開始Idx]
        新しい開始Vノード = 新しいCh[++新しい開始Idx]
      } そうでない場合 (sameVnode(oldEndVnode, newEndVnode)) {
        パッチVnode(古い終了Vnode、新しい終了Vnode、挿入されたVnodeQueue、新しいCh、新しい終了Idx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnodeが右に移動
        パッチVnode(古い開始Vnode、新しい終了Vnode、挿入されたVnodeQueue、新しいCh、新しい終了Idx)
        canMove && nodeOps.insertBefore(親Elm、oldStartVnode.elm、nodeOps.nextSibling(oldEndVnode.elm))
        古い開始Vノード = 古いCh[++古い開始Idx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnodeが左に移動
        パッチVnode(古い終了Vnode、新しい開始Vnode、挿入されたVnodeQueue、新しいCh、新しい開始Idx)
        canMove && nodeOps.insertBefore(親Elm、古い終了Vnode.elm、古い開始Vnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        新しい開始Vノード = 新しいCh[++新しい開始Idx]
      } それ以外 {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? 古いキーからIdx[新しい開始Vノード.キー]
          : findIdxInOld(新しい開始ノード、古いCh、古い開始Idx、古い終了Idx)
        if (isUndef(idxInOld)) { // 新しい要素
          createElm(newStartVnode、insertedVnodeQueue、parentElm、oldStartVnode.elm、false、newCh、newStartIdx)
        } それ以外 {
          vnodeToMove = 古いCh[idxInOld]
          同じVnode(vnodeToMove, newStartVnode)の場合
            パッチVnode(vnodeToMove、newStartVnode、insertedVnodeQueue、newCh、newStartIdx)
            oldCh[idxInOld] = 未定義
            canMove && nodeOps.insertBefore(親Elm、vnodeToMove.elm、oldStartVnode.elm)
          } それ以外 {
            // 同じキーだが要素が異なる。新しい要素として扱う
            createElm(newStartVnode、insertedVnodeQueue、parentElm、oldStartVnode.elm、false、newCh、newStartIdx)
          }
        }
        新しい開始Vノード = 新しいCh[++新しい開始Idx]
      }
    }
    (古い開始ID>古い終了ID)の場合{
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      Vnodesを追加します(parentElm、refElm、newCh、newStartIdx、newEndIdx、insertedVnodeQueue)
    } そうでない場合 (newStartIdx > newEndIdx) {
      Vnodes を削除します (古い Ch、古い開始 ID、古い終了 ID)
    }
  }

ここでは、まず、oldStartIdx (古いノードの最初のインデックス)、oldEndIdx (古いノードの最後のインデックス)、oldStartVnode (古いノードの最初の要素)、oldEndVnode (古いノードの最後の要素) などのいくつかのパラメータを定義します。同様に、newStartIdx と他の 4 つの項目は、新しいノードの最初のインデックスなどです。

whileループ内の操作を見てください。これもコアコンテンツです。

同じノードであると判断した後、ノードはpatchVnodeメソッドを続行する必要がある。

  • 古い最初の要素と新しい最初の要素が同じノードである場合、古い最初のインデックスと新しい最初のインデックスは同時に右に移動します。
  • 古い末尾要素と新しい末尾要素が同じノードである場合、古い末尾インデックスと新しい末尾インデックスは同時に左にシフトされます。
  • 古い最初の要素と新しい最後の要素が同じノードにある場合、メソッドによってアップロードされた読み取り専用の判定に従って、それが false であれば、古い最初の要素を古いノードの最後のインデックスに移動し、同時に古い最初のインデックスを右に移動し、新しい最後のインデックスを左に移動します。
  • 古い末尾要素と新しい最初の要素が同じノードにある場合、メソッドによってアップロードされた読み取り専用判定に従って、それが偽であれば、古い末尾要素を古いノードの前の位置に移動し、同時に古い末尾インデックスを左に移動し、新しい最初のインデックスを右に移動します。
  • 上記の条件がいずれも満たされない場合は、oldCh に newStartVnode と同じキーを持つ Vnode があるかどうかを判断します。見つからない場合は、新しいノードであることを意味します。新しいノードを作成して挿入します。

newStartVnode と同じキーを持つ Vnode が見つかった場合、その Vnode は vnodeToMove と名付けられ、次に vnodeToMove が newStartVnode と比較されます。同じ場合は、両方が再度パッチされます。removeOnly が false の場合、newStartVnode と同じキーを持つ vnodeToMove.elm という名前の Vnode が、oldStartVnode.elm の前に移動されます。キー値は同じでもノードが異なる場合は、新しいノードが作成されます。

While ループの後、新しいノード配列または古いノード配列にまだノードが残っている場合は、具体的な状況に応じてそれらを削除または追加します。

oldStartIdx > oldEndIdxの場合、oldChが最初に走査され、冗長な新しいノードが存在することを意味します。新しいノードを追加します

newStartIdx > newEndIdx の場合、新しいノードが最初に走査され、古いノードがまだ残っているため、残りのノードを削除します。

下の例の写真を見てみましょう

元のノード (oldVnode は古いノード、Vnode は新しいノード、diff は diff アルゴリズムの後に生成されたノード配列です)

最初にループすると、古い末尾要素が新しい最初の要素と同じであることがわかるので、古い末尾要素 D は古い最初のインデックスの先頭、つまり A の前に移動されます。同時に、古い末尾インデックスは左に移動し、新しい最初のインデックスは右に移動します。

2 回目のループでは、新しい最初の要素は古い最初の要素と同じになります。このとき、2 つの要素の位置は変化せず、新しい最初のインデックスと古い最初のインデックスは同時に右に移動します。

3 番目のループでは、古い要素に現在の要素と同じノードが存在しないことが検出されるため、新しいノードが追加され、古い最初の要素の前に F が配置されます。同様に、4 番目のループも同じです。2 つのループの後、新しいサンプル グラフが生成されます。

5 番目のサイクルは 2 番目のサイクルと同じです。

6番目のループでは、newStartIdxが再び右に移動します

7. 最後の移動の後、newStartIdx > newEndIdxとなり、whileループは終了しました。これは、newChが最初に走査され、oldChにはまだ余分なノードがあり、それらは直接削除されることを証明しています。そのため、最後のノードは

上記は、diffアルゴリズムとdiffアルゴリズムの実装プロセスに関連するいくつかの機能です。

結論

差分アルゴリズムは仮想 DOM の中核部分です。同じレイヤーを比較し、新しいノードと古いノードを比較して変更された部分を実際の DOM に更新します。

具体的な実装方法は、patch、patchVnode、updateChildrenです。

パッチの核となるのは、新しいノードは存在するが古いノードは存在しない場合はそれを追加すること、古いノードは存在するが新しいノードは存在しない場合はそれを削除すること、両方が存在する場合はそれらが同じかどうかを判断し、同じ場合は比較の次のステップのために patchVnode を呼び出すことです。

patchVnode の核となるのは、新しいノードと古いノードがコメントやテキスト ノードではなく、新しいノードに子ノードがあるが古いノードにはない場合は子ノードを追加する、新しいノードに子ノードがなく古いノードに子ノードがある場合は古いノードの下の子ノードを削除する、両方に子ノードがある場合は updateChildren メソッドを呼び出す、というものです。
updateChildren の中核は、新しいノードと古いノードを比較し、それらを追加、削除、または更新することです。

これは、Vue2.0 の diff アルゴリズムの予備的な説明にすぎません。より深い原理と、Vue3.0 の diff アルゴリズムに変更があるかどうかについては、まだ解明されていません。

Vue の diff アルゴリズムに関するこの記事はこれで終わりです。Vue の diff アルゴリズムの詳細については、123WORDPRESS.COM の以前の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • Vue の仮想 DOM と diff アルゴリズムをご存知ですか?
  • Vue diffアルゴリズムの完全な分析
  • Vue2のdiffアルゴリズムの詳細な説明
  • vue3.0 diffアルゴリズムの使用に関する詳細な説明(超詳細)
  • Vueのdiffアルゴリズム原理の詳細な説明
  • Vue の diff アルゴリズムの原理を本当に理解していますか?

<<:  MySQL 8.0.20 のインストールと設定方法のグラフィックチュートリアル

>>:  Linux での MongoDB のインストールと設定のチュートリアル

推薦する

時間別にグループ化された MySQL クエリ ステートメント

年、月、週、日グループによる MySQL クエリ1. 学年別検索 SELECT DATE_FORMA...

Linux 上で Docker コンテナを作成、一覧表示、削除する方法の概要

1. Dockerコンテナを起動する以下のコマンドを使用して新しい Docker コンテナを起動しま...

HTML 要素 (タグ) とその使用法

a : ハイパーリンクの開始位置または宛先位置を示します。頭字語: 単語の最初の文字からなる略語を示...

Docker クリーニングの一般的な方法と問題点

大規模な開発に Docker を使用する場合でも、クリーンアップ戦略がなければ、ディスクがすぐにいっ...

MySQL InnoDB 監視 (システム層、データベース層)

MySQL InnoDB 監視 (システム層、データベース層) MySQL の監視に関しては、My...

Linux の MariaDB データベースについて

目次Linux の MariaDB データベースについて1. データベースとは何ですか? 2. デー...

Vue カプセル化 TabBar コンポーネントの完全なステップ記録

目次実装のアイデア:ステップ 1: TabBar と TabBarItem のコンポーネント カプセ...

NodeサイトのForever+nginx導入方法例

私は最近、最も安い Tencent クラウド サーバーを購入しました。これは主に、Web テクノロジ...

MySQLデータをOracleに移行する正しい方法

mysql データベースには student テーブルがあり、その構造は次のとおりです。 Oracl...

Apple 電卓の JS 実装

この記事の例では、Appleの電卓を実装するためのJSの具体的なコードを参考までに共有しています。具...

Vue ミックスインの詳しい説明

目次ローカルミックスイングローバル ミックスイン要約するローカルミックスイン <テンプレート&...

JavaScript データ型変換の例 (他の型を文字列、数値型、ブール型に変換する)

序文データ型変換とは何ですか?フォームまたはプロンプトを使用して取得されるデフォルトのデータ型は文字...

MySQL がデフォルト値を持つ NULL 列の使用を推奨しない理由

よく聞かれる答えは、列に NULL 値を使用するとインデックスが無効になるというものですが、実際にテ...

Tomcatがセッションを管理する方法の例

ConcurrentHashMapを学習しましたが、どのように適用すればよいかわかりませんか? To...