Vue でインデックスをキー属性値として使用することが推奨されないのはなぜですか?

Vue でインデックスをキー属性値として使用することが推奨されないのはなぜですか?

序文

フロントエンド開発では、リストのレンダリングが関係する限り、React フレームワークでも Vue フレームワークでも、各リスト項目に一意のキーを使用するように求められたり要求されたりします。多くの開発者は、キーの原理を知らずに、配列インデックスをキー値として直接使用します。この記事では、キーの役割と、キーの属性値としてインデックスを使用しないことが最適な理由について説明します。

キーの役割

Vue は仮想 DOM を使用し、diff アルゴリズムに従って古い DOM と新しい DOM を比較して実際の DOM を更新します。キーは仮想 DOM オブジェクトの一意の識別子です。キーは diff アルゴリズムで非常に重要な役割を果たします。

差分アルゴリズムにおけるキーの役割

実際、React と Vue の diff アルゴリズムはほぼ同じですが、diff の比較方法はまだかなり異なり、各バージョンの diff もかなり異なります。次に、Vue3.0のdiffアルゴリズムを出発点として、diffアルゴリズムにおけるキーの役割を分析します。

具体的な差分処理は以下のとおりです

写真

Vue3.0ではpatchChildrenメソッドにこのようなソースコードがあります

パッチフラグ > 0 の場合
      パッチフラグとパッチフラグ.KEYED_FRAGMENTの場合{ 
         /* キーが存在する場合は、diffアルゴリズムを使用します */
        パッチキー付き子供(
         ...
        )
        戻る
      } そうでない場合 (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
         /* キーが存在しない場合は直接パッチを適用します */
        パッチUnkeyedChildren( 
          ...
        )
        戻る
      }
    }

patchChildren は、キーが存在するかどうかに応じて、実際の diff または直接パッチを実行します。キーが存在しないケースについては詳しく説明しません。

まず、宣言された変数をいくつか見てみましょう。

/* c1 古い vnode c2 新しい vnode */
let i = 0 /* レコードインデックス */
const l2 = c2.length /* 新しい vnode の数*/
let e1 = c1.length - 1 /* 古いvnodeの最後のノードのインデックス*/
let e2 = l2 - 1 /* 新しいノードの最後のノードのインデックス*/

ヘッドノードを同期する

最初のステップは、最初から同じ vnode を見つけて、それをパッチすることです。同じノードでない場合は、すぐにループから抜け出します。

//(ab) c
//(ab)デ
/* 最初から比較して同じノードパッチを見つけ、異なる場合はすぐにジャンプします*/
    i <= e1 && i <= e2 の場合
      定数 n1 = c1[i]
      const n2 = (c2[i] = 最適化
        ? cloneIfMounted(c2[i]をVNodeとして)
        : VNodeを正規化する(c2[i])
        /* キーと型が等しいかどうかをチェックします*/
      (isSameVNodeType(n1, n2)) の場合 {
        パッチ(
          ...
        )
      } それ以外 {
        壊す
      }
      私は++
    }

プロセスは次のとおりです。

写真

isSameVNodeTypeは、現在のvnodeタイプがvnodeキーと等しいかどうかを判断するために使用されます。

エクスポート関数 isSameVNodeType(n1: VNode, n2: VNode): boolean {
  n1.type === n2.type && n1.key === n2.key を返します
}

実際、これを見ると、diff アルゴリズムにおけるキーの役割、つまり同じノードであるかどうかを判断することがすでにわかっています。

テールノードを同期する

2番目のステップは、前と同じ差分で最後から始まります。

//a (bc)
//de (bc)
/* 最初のステップがパッチされていない場合は、すぐに後ろから前に向かってパッチを当て始めます。違いが見つかった場合は、すぐにループから抜け出します*/
    i <= e1 && i <= e2 の場合
      定数 n1 = c1[e1]
      const n2 = (c2[e2] = 最適化
        ? cloneIfMounted(c2[e2]をVNodeとして)
        : VNodeを正規化する(c2[e2])
      (isSameVNodeType(n1, n2)) の場合 {
        パッチ(
         ...
        )
      } それ以外 {
        壊す
      }
      e1--
      e2--
    }

最初のステップの後、パッチが完全ではないことが判明した場合は、すぐに 2 番目のステップに進み、最後から始めて前方に diff を走査します。同じノードでない場合は、すぐにループから抜け出します。プロセスは次のとおりです。

新しいノードの追加

ステップ3: 古いノードがすべてパッチ適用され、新しいノードがパッチ適用されていない場合は、新しいvnodeを作成します。

//(アブ)
//(ab) c
//i = 2、e1 = 1、e2 = 2
//(アブ)
//タクシー)
//i = 0、e1 = -1、e2 = 0
/* 新しいノードの数が古いノードの数より多い場合、残りのすべてのノードは新しい vnode として処理されます (つまり、同じ vnode がパッチ適用されたことになります) */
    もし(i>e1){
      もし(i <= e2){
        定数 nextPos = e2 + 1
        const アンカー = nextPos < l2 ? (c2[nextPos] を VNode として).el : parentAnchor
        i <= e2 の場合
          patch( /* 新しいノードを作成する */
            ...
          )
          私は++
        }
      }
    }

プロセスは次のとおりです。

写真

冗長ノードを削除する

ステップ 4: すべての新しいノードにパッチが適用され、古いノードが残っている場合は、すべての古いノードをアンインストールします。

//i>e2 です
//(ab) c
//(アブ)
//i = 2、e1 = 2、e2 = 1
//a (bc)
//(紀元前)
//i = 0、e1 = 0、e2 = -1
そうでない場合 (i > e2) {
   i <= e1 の場合
      アンマウント(c1[i], 親コンポーネント, 親サスペンス, true)
      私は++
   }
}

プロセスは次のとおりです。

最長増加部分列

この時点ではまだ核となるシーンは現れていません。運が良ければここで終わるかもしれませんが、完全に運に頼ることはできません。残りのシナリオは、新しいノードと古いノードの両方に複数の子ノードがあることです。次に、Vue3 がどのようにそれを実行するかを見てみましょう。移動、追加、アンインストールの操作を組み合わせる

要素を移動するたびに、パターンを見つけることができます。要素を移動する回数を最小限に抑えるには、一部の要素を安定させる必要があります。では、安定した状態を維持できる要素のパターンは何でしょうか。

上記の例を見てください: c h d e VS d e i c。比較すると、c を末尾に移動し、h をアンインストールして i を追加するだけでよいことが肉眼でわかります。 d e は変更されないままです。新旧ノードの d e の順序は変更されていないことがわかります。d は e の後にあり、添え字は増加状態にあります。

ここでは、最長増加部分列と呼ばれる概念を紹介します。
公式の説明: 指定された配列で、可能な限り最大の長さを持つ増加する値のセットを見つけます。
少し分かりにくいので、具体的な例を見てみましょう。

定数arr = [10, 9, 2, 5, 3, 7, 101, 18]
=> [2, 3, 7, 18]
この配列はarrの最長増加部分列であり、[2, 3, 7, 101]も同様です。
したがって、最長の増加部分列は次の 3 つの要件を満たします。
1. 部分列の値は増加している
2. 部分列の値の添え字は元の配列では増加している
3. このサブシーケンスは見つけられる最も長いものですが、通常は、より小さい値を持つシーケンスのセットを見つけます。これは、より大きくなる余地があるためです。

次のアイデアは、新しいノード シーケンス内の順序が変更されないノードを見つけることができれば、移動する必要がないノードがわかり、ここにないノードのみを挿入すればよいというものです。最終的に提示される順序は新しいノードの順序であり、移動は古いノードの移動にすぎないため、古いノードが最長順序を変更せずに維持する限り、個々のノードを移動することで、その順序との一貫性を保つことができます。そのため、その前に、まずすべてのノードを見つけて、対応するシーケンスを見つけます。最終的に実際に取得したいのは、この配列です: [2, 3, new, 0]。実はこれがdiffの動きの考え方です

写真

インデックスを使用しないのはなぜですか?

パフォーマンスコスト

インデックスをキーとして使用すると、各ノードが対応するキーを見つけることができず、一部のノードを再利用できず、すべての新しい vnode を再作成する必要があるため、順次操作が破棄されます。

例:

<テンプレート>
  <div class="hello">
    <ul>
      <li v-for="(item,index) in studentList" :key="index">{{item.name}}</li>
      <br>
      <button @click="addStudent">データを追加する</button>
    </ul>
 
  </div>
</テンプレート>
 
<スクリプト>
エクスポートデフォルト{
  名前: 'HelloWorld',
  データ() {
    戻る {
      学生リスト: [
        { id: 1, 名前: '张三', 年齢: 18 },
        { id: 2、名前: 'Li Si'、年齢: 19 }、
      ]、
    };
  },
  方法:{
    生徒を追加します(){
      const studentObj = { id: 3, name: '王五', age: 20 };
      this.studentList=[studentObj,...this.studentList]
    }
  }
}
</スクリプト>

まず Chorme デバッガーを開き、ダブルクリックして内部のテキストを変更してみましょう。

写真

上記のコードを実行して結果を確認してみましょう。

写真

上記の実行結果から、追加したのは 1 つのデータだけであるのに、3 つのデータすべてを再レンダリングする必要があることがわかります。驚きではありませんか? 明らかに 1 つのデータだけを挿入したのに、なぜ 3 つのデータすべてを再レンダリングする必要があるのでしょうか?私が望んでいるのは、新しく追加されたデータをレンダリングすることだけです。

上記では、diff 比較方法についても説明しました。次に、diff 比較に基づいて図を描いて、比較がどのように行われるかを見てみましょう。

写真

先頭にデータを追加すると、インデックスの順序が中断され、新しいノードのすべてのキーが変更されるため、ページ上のデータが再レンダリングされます。

次に、インデックスを使用する場合と使用しない場合のパフォーマンスを比較するために、1000 個の DOM を生成します。キーの一意性を保証するために、キーとして uuid を使用します。

インデックスをキーとして1回実行してみましょう

<テンプレート>
  <div class="hello">
    <ul>
      <button @click="addStudent">データを追加する</button>
      <br>
      <li v-for="(item,index) in studentList" :key="index">{{item.id}}</li>
    </ul>
  </div>
</テンプレート>
 
<スクリプト>
'uuid/v1' から uuidv1 をインポートします。
エクスポートデフォルト{
  名前: 'HelloWorld',
  データ() {
    戻る {
      学生リスト: [{id:uuidv1()}],
    };
  },
  作成された(){
    (i = 0; i < 1000; i++ とします) {
      this.studentList.push({
        id: uuidv1(),
      });
    }
  },
  更新前(){
    コンソールで時間を指定します。
  },
  更新されました(){
    console.timeEnd('for') //for: 75.259033203125 ミリ秒
  },
  方法:{
    生徒を追加します(){
      定数studentObj = { id: uuidv1() };
      this.studentList=[studentObj,...this.studentList]
    }
  }
}
</スクリプト>

IDをキーとして置き換える

<テンプレート>
  <div class="hello">
    <ul>
      <button @click="addStudent">データを追加する</button>
      <br>
      <li v-for="(item,index) in studentList" :key="item.id">{{item.id}}</li>
    </ul>
  </div>
</テンプレート>
  更新前(){
    コンソールで時間を指定します。
  },
  更新されました(){
    console.timeEnd('for') //for: 42.200927734375 ミリ秒
  },

上記の比較から、一意の値をキーとして使用するとオーバーヘッドを節約できることがわかります。

データの不整合

上記の例では、インデックスをキーとして使用すると、ページの読み込み効率にのみ影響し、少量のデータでは影響が小さいと思われるかもしれません。次のケースでは、インデックスを使用すると予期しない問題が発生する可能性があります。上記のシナリオでも、最初に各テキストコンテンツの後に入力ボックスを追加し、入力ボックスに手動でコンテンツを入力してから、ボタンを使用してクラスメートを前方に追加します。

<テンプレート>
  <div class="hello">
    <ul>
      <li v-for="(item,index) in studentList" :key="index">{{item.name}'',input /></li>
      <br>
      <button @click="addStudent">データを追加する</button>
    </ul>
  </div>
</テンプレート>
 
<スクリプト>
エクスポートデフォルト{
  名前: 'HelloWorld',
  データ() {
    戻る {
      学生リスト: [
        { id: 1, 名前: '张三', 年齢: 18 },
        { id: 2、名前: 'Li Si'、年齢: 19 }、
      ]、
    };
  },
  方法:{
    生徒を追加します(){
      const studentObj = { id: 3, name: '王五', age: 20 };
      this.studentList=[studentObj,...this.studentList]
    }
  }
}
</スクリプト>

入力にいくつかの値を入力し、クラスメートを追加して効果を確認してみましょう。

写真

この時点で、追加前に入力したデータが間違っていることがわかります。追加した後、張三の情報が王武の入力ボックスに残りますが、これは明らかに私たちが望んでいる結果ではありません。

写真

上の比較から、インデックスをキーにしているため、比較するとテキスト値が変化していることがわかりますが、下方向に比較し続けると、Input DOM ノードは以前と同じであることがわかります。そのため、再利用されます。ただし、入力ボックスが入力値を保持することは予想外であり、このとき、入力値が間違って配置されます。

解決

インデックスを使用すると場合によっては非常に悪い影響が出る可能性があることがわかっているので、開発中にこの問題をどのように解決すればよいでしょうか?実際、キーが一意で変更されていない限り、開発では次の 3 つの状況がより頻繁に使用されます。

  • 開発では、ID、携帯電話番号、ID 番号など、バックグラウンドによって返される一意の値など、各データのキーとして一意の識別子を使用するのが最適です。
  • キーとして Symbol を使用できます。Symbol は ES6 で導入された新しいプリミティブ データ型です。これは一意の値を表し、主にオブジェクトの一意のプロパティ名を定義するために使用されます。
a=Symbol('test') とします。
b = シンボル('テスト')とします。
console.log(a===b) //偽

キーとして uuid を使用できます。uuid は Universally Unique Identifier の略です。これは、特定の範囲 (特定の名前空間から世界まで) 内で一意である、機械によって生成された識別子です。

上記の最初の解決策をキーとして使用し、図に示すように、上記の状況をもう一度見てみます。同じキーを持つノードは再利用されます。これが diff アルゴリズムの実際の効果です。

写真

写真

写真

要約する

  • インデックスをキーとして使用すると、データが逆の順序で追加または削除されたときに不要な実際の DOM 更新が生成され、効率が低下します。
  • インデックスをキーとして使用する場合、構造に入力クラスのDOMが含まれていると、不正なDOM更新が発生します。
  • 開発では、ID、携帯電話番号、ID 番号など、バックグラウンドによって返される一意の値など、各データのキーとして一意の識別子を使用するのが最適です。
  • 逆順にデータを追加したり削除したりするなど、順序を崩す操作がなく、レンダリングや表示のみに使用する場合は、インデックスをキーとして使用することも可能です (ただし、良好な開発習慣を身に付けるためには、やはり推奨されません)。

これで、Vue でインデックスをキーとして使用することが推奨されない理由に関するこの記事は終了です。Vue のインデックスをキーとして使用することに関する関連コンテンツの詳細については、123WORDPRESS.COM の以前の記事を検索するか、次の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM を応援していただければ幸いです。

以下もご興味があるかもしれません:
  • Vue でインデックスをキー属性値として使用することが推奨されないのはなぜですか?
  • Vue で v-for を使用するときに、インデックスをキーとして使用できないのはなぜですか?
  • Vue でインデックスをキーとして使用すべきでない理由の詳細な説明 (diff アルゴリズム)
  • Vue 2.0 の v-for 反復構文の変更点 (キー、インデックス) に関する簡単な説明
  • vue2.0 で削除または変更された項目 (インデックス キーが削除されました)

<<:  CSS パフォーマンスの最適化 - will-change の使用方法の詳細な説明

>>:  Mysql クエリの結果セットを JSON データに変換するサンプル コード

推薦する

MySQL が「operate_time」エラーのデフォルト値が無効であると報告する問題を解決する

データベースでcreate tableステートメントを実行する テーブル `sys_acl` を作成...

HTMLテーブルではテーブルの外側の境界線のみが表示されます

質問があります。Dreamweaver で、3 行 1 列のログイン フォーム (ログイン、登録、パ...

MySQLトリガーの使用と理解

目次1. トリガーとは何ですか? 2. トリガーを作成するトリガーを作成するための構文は次のとおりで...

Linux ファイル管理コマンド例の分析 [表示、閲覧、統計など]

この記事では、Linux ファイル管理コマンドについて例を挙げて説明します。ご参考までに、詳細は以下...

数ステップでサイバーパンク2077風の視覚効果を実現するCSS

背景記事を始める前に、賽博朋克とは何か、賽博朋克2077とは何かを簡単に理解しましょう。サイバーパン...

リバースプロキシ設定を実装するためのユニバーサルnginxインターフェース

1. プロキシサーバーとは何ですか?プロキシ サーバーは、クライアントが要求を送信すると、それを直接...

JavaScript でよく使われるいくつかの文字列メソッドの概要 (初心者必読)

JavaScriptでよく使われるいくつかの文字列メソッド文字列は読み取り専用データです。よく使用...

FileZilla 425 FTP に接続できない (Alibaba クラウド サーバー) の解決策

Alibaba Cloud ServerがFTPに接続できないFileZilla 425 データ接続...

MySql データベースにリモートでログインするにはどうすればよいですか?

はじめに: プロジェクトを開発するために、サーバーに MySql データベース サーバーを展開し、ロ...

Centos 7.4 でリモート アクセス制御を実装する方法

1. SSHリモート管理SSH はセキュア チャネル プロトコルであり、主にリモート ログイン、リモ...

dockerでifconfigが利用できない問題を解決する

最近、docker を学習していたときに、docker コンテナ内のネットワーク状態を照会するために...

js でパズルゲームを実装する

この記事では、パズルゲームを実装するためのjsの具体的なコードを参考までに共有します。具体的な内容は...

Vue でスクロールバーのスタイルを変更する方法

目次まず、スクロール バーのスタイルを変更するには、疑似要素-webkit-scrollbarを使用...

モバイルウェブサイトの開発に関するいくつかの結論

ウェブサイトのモバイル版には、少なくともいくつかの基本機能が必要です。 1. ページの適用性の問題:...

Linux でユーザーをグループに追加する 4 つの方法の概要

序文Linux グループは、Linux でユーザー アカウントを管理するために使用される組織単位です...