React diffアルゴリズムソースコード分析

React diffアルゴリズムソースコード分析

React の Diff アルゴリズムは、調整アルゴリズムとも呼ばれます。対応する関数はreconcileChildrenと呼ばれます。その主な機能は、更新プロセス中に変更された要素をマークすることです。これらの変更には、追加、移動、削除が含まれます。このプロセスはbeginWorkフェーズで発生し、Diff は初期レンダリングでない場合にのみ実行されます。

以前、Diff アルゴリズムを 2 つの Fiber ツリーの比較として説明する記事をいくつか読んだことがあります。これは誤りです。実際の Diff プロセスは、既存の Fiber ノードのセットとJSXによって生成された新しいReactElementを比較し、新しい Fiber ノードを生成するというものです。このプロセスでは、既存の Fiber ノードの再利用も試みられます。

Node Diff は次の 2 つのタイプに分かれています。

  1. 単一ノードの差分 - ElementPortalstringnumber
  2. マルチノード Diff - ArrayIterator

次の React バージョンは 17.0.1、コード ファイルは ReactChildFiber.old.js です。

単一ノード差分

単一ノードの Diff は比較的単純です。キーとタイプが同じ場合にのみノードを再利用しようとし、そうでない場合は新しいノードを返します。

ほとんどの場合、単一のノードにキーを割り当てないので、デフォルトでは null になり、同じになります。

単一要素を調整する

  // 単一ノード比較関数 reconcileSingleElement(
    returnFiber: ファイバー、
    currentFirstChild: ファイバー | null、
    要素: ReactElement、
    レーン: レーン、
  ): ファイバー {
    // 新しい reactElement のキー
    定数キー = 要素.キー;
    //現在の子ファイバーノード let child = currentFirstChild;
    while (child !== null) {
      // キーが同じ場合のみ比較
      if (child.key === key) {
        スイッチ(子タグ){
          // ...
          デフォルト: {
            // 現在のファイバーと reactElement が同じタイプの場合、if (child.elementType === element.type) {
              // 同じレベルの他のノードを削除します deleteRemainingChildren(returnFiber, child.sibling);
              // 現在の子ファイバーを再利用します
              const existing = useFiber(child, element.props);
              existing.ref = coerceRef(returnFiber、child、element);
              既存の.return = returnFiber;
              // 再利用可能な子ファイバーを返す
              既存のものを返します。
            }
            壊す;
          }
        }
        // 一致しないノードを削除します deleteRemainingChildren(returnFiber, child);
        壊す;
      } それ以外 {
        // キーが異なる場合はノードを直接削除します deleteChild(returnFiber, child);
      }
      子 = 子.兄弟;
    }

    // 新しいファイバーノード const created = createFiberFromElement(element, returnFiber.mode, lanes);
    created.ref = coerceRef(returnFiber、currentFirstChild、要素);
    作成された戻り値 = returnFiber;
    戻り値が作成されました。
  }

マルチノード差分

ソース コードは、複数のノードを配列ノードと反復可能なノードに分割します。

if (isArray(newChild)) {
  reconcileChildrenArray() を返す
    リターンファイバー、
    現在の最初の子、
    新しい子、
    車線、
  );
}

(getIteratorFn(新しい子))の場合{
  reconcileChildrenIterator() を返す
    リターンファイバー、
    現在の最初の子、
    新しい子、
    車線、
  );
}

対応する Diff 関数はreconcileChildrenArrayreconcileChildrenIteratorです。コアとなる Diff ロジックは同じなので、配列ノードの Diff のみが分析されます ( reconcileChildrenArray関数)。

このコードセクションは比較的長いですが、ロジックは非常に明確であり、分割線から 2 ラウンドのトラバースに分割されています。

  • 最初の走査ラウンドは、更新する必要がある同じ順序と同じキーを持つノードです。
  • 2 回目の走査では、順序が異なり、キーも異なる可能性のあるノードを検索します。これらのノードは、追加、移動、または削除する必要があります。

最初のトラバーサル ラウンドは、キーと順序が同じ場合のみに行われます。これらのキーに対応するノードの位置は変更されていないため、更新操作のみが必要です。トラバーサルで異なるキーに遭遇すると、ループから抜け出す必要があります。

// 古いノード <li key="0"/>
<li キー="1"/>
<li キー="2"/>
// 新しいノード <li key="0"/>
<li キー="1"/>
<li キー="5"/>

// key="5" は異なるため、トラバーサルから抜ける // トラバーサル ノードの最初のラウンド <li key="0"/>
<li キー="1"/>
// <li key="2"/> と <li key="5"/> は、2 回目の走査と比較で残ります。

最初のトラバースラウンドの後には、2 つの状況があります。

  1. 新しいノードの数は古いノードの数より少ないです。この時点で、冗長な古いノードを削除対象としてマークする必要があります。
  2. 新しいノードの数が古いノードの数より多い場合、余分な新しいノードは新しく追加されたものとしてマークする必要があります。

2 回目の走査は、異なるキーまたは異なる順序に対して行われます。考えられる状況は次のとおりです。

// 古いノード <li key="0"/>
<li キー="1"/>
<li キー="2"/>
// 新しいノード <li key="0"/>
<li キー="2"/>
<li キー="1"/>

// 2 回目の走査では、2 つのノード <li key="2"/> と <li key="1"/> を比較します。

2 回目のトラバーサルは少し複雑になりますが、これについては後で詳しく説明します。

詳細なコードは以下のとおりです。

調整子配列

  関数 reconcileChildrenArray(
    returnFiber: ファイバー、
    currentFirstChild: ファイバー | null、
    新しい子: 配列<*>,
    レーン: レーン、
  ): ファイバー | null {
    // 関数によって返されるファイバー ノード let resultsingFirstChild: Fiber | null = null;
    previousNewFiber: Fiber | null = null; とします。

    // oldFiber はリンク リストです。let oldFiber = currentFirstChild;
    // ノードが移動されたかどうかを判断するために使用されます。let lastPlacedIndex = 0;
    newIdx = 0 とします。
    nextOldFiber を null にします。
    // 最初の走査では、同じキーを持つノードのみが走査されます for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      (oldFiber.index > newIdx) の場合 {
        次の古いファイバー = 古いファイバー;
        古いファイバー = null;
      } それ以外 {
        // 古いファイバー ノードがループされるたびに、次のループのファイバー ノードである兄弟要素を指します。 nextOldFiber = oldFiber.sibling;
      }
      // キーが同じ場合はファイバーノードを返します。キーが異なる場合は null を返します。
      // 型が同じ場合はノードを再利用し、そうでない場合は新しいノードを返します。const newFiber = updateSlot(
        リターンファイバー、
        古いファイバー、
        新しい子供[newIdx]、
        車線、
      );
      // newFiber は null で、キーが異なることを示し、ループは終了します if (newFiber === null) {
        (古いファイバーがnullの場合){
          古いファイバー = 次の古いファイバー;
        }
        壊す;
      }
      // newFiber.alternate が null の場合、それは新しいノードであり、異なるタイプの新しいファイバーノードが作成されたことを示します。if (oldFiber && newFiber.alternate === null) {
        // oldFiber タグを削除する必要があります deleteChild(returnFiber, oldFiber);
      }
      // ノードを配置し、lastPlacedIndexを更新します
      lastPlacedIndex = placeChild(newFiber、lastPlacedIndex、newIdx);
      // 新しいファイバーノードチェーンを形成する if (previousNewFiber === null) {
        結果として得られるFirstChild = newFiber;
      } それ以外 {
        前のNewFiber.sibling = newFiber;
      }
      前のNewFiber = 新しいFiber;
      古いファイバー = 次の古いファイバー;
    }

    /*
    最初の走査ラウンドの後、新しいノードの数は古いノードの数よりも少なくなります。newChildren は走査され、残りのファイバーノードは削除されます。考えられる状況は次のとおりです⬇️
    <li key="0"/> の前
    <li キー="1"/>
    <li キー="2"/>
    新規 <li key="0"/>
    <li キー="1"/>
    <li key="2"/> は削除されます*/
    (newIdx === newChildren.length) の場合 {
      残りの子供を削除します(returnFiber、oldFiber);
      結果のFirstChildを返します。
    }

    /*
    最初のトラバースラウンドの後、新しいノードの数が古いノードの数より多くなっています。OldFiber がトラバースされました。考えられる状況は次のとおりです⬇️
    <li key="0"/> の前
    <li キー="1"/>
    新規 <li key="0"/>
    <li キー="1"/>
    <li キー="2"/>
    新しい <li key="2"/> が追加されます。このセクションは、新しいノードの挿入ロジックです*/
    (古いファイバーがnullの場合){
      (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx], レーン);
        (newFiber === null)の場合{
          続く;
        }
        lastPlacedIndex = placeChild(newFiber、lastPlacedIndex、newIdx);
        // 新しいファイバーノードチェーンを形成する if (previousNewFiber === null) {
          結果として得られるFirstChild = newFiber;
        } それ以外 {
          前のNewFiber.sibling = newFiber;
        }
        前のNewFiber = 新しいFiber;
      }
      結果のFirstChildを返します。
    }
      
    // ---------------------------------------------------------------------

    // 残りの oldFiber を使用して、key->fiber ノード マップを作成し、対応する古いファイバー ノードを key で取得できるようにします。const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    
    // 2 回目のトラバーサル。残りのノードのトラバーサルを続けます。これらのノードは移動または削除する必要がある可能性があります。for (; newIdx < newChildren.length; newIdx++) {
      // マップからキーに対応する古いノードを取得し、更新された新しいノードを返します。const newFiber = updateFromMap(
        既存の子供、
        リターンファイバー、
        新しいIDx、
        新しい子供[newIdx]、
        車線、
      );
      newFiber !== null の場合 {
        // 新しいノードを再利用し、古いノードをマップから削除します。対応する状況は位置の変更である可能性があります if (newFiber.alternate !== null) {
          // 再利用されたノードはマップから削除する必要があります。マップ内の残りのノードは削除としてマークされるためです existingChildren.delete(
            newFiber.key === null ? newIdx : newFiber.key,
          );
        }
        // ノードを配置し、移動するかどうかを決定します lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (以前の新しいファイバー === null) {
          結果として得られるFirstChild = newFiber;
        } それ以外 {
          前のNewFiber.sibling = newFiber;
        }
        前のNewFiber = 新しいFiber;
      }
    }

    // 残りの不要なノードを削除します existingChildren.forEach(child => deleteChild(returnFiber, child));

    結果のFirstChildを返します。
  }

最初のトラバーサルは比較的簡単に理解できます。ここでは、2 回目のトラバーサルを詳細に分析します。2 回目のトラバーサルでは、再利用を移動する必要があるかどうかという疑問が生じるためです。

2 回目のトラバーサルでは、まず残りのoldFiberをトラバースし、 key -> 舊fiber節點的Mapを形成します。これを使用して、キーを通じて古いノードをすばやく取得できます。

次のトラバーサルでは、新しいノードをトラバーサル オブジェクトとして使用します。トラバーサルでは、新しいノードのキーを使用してマップから古いノードを取得し、再利用できるかどうかを比較するたびに、新しいノードのキーを使用します。対応する関数はupdateFromMapです。

ノードにalternate属性がある場合、それは再利用ノードです。この時点で、 existingChildrenから削除する必要があります。その後、2 回目のトラバーサル後にexistingChildrenにまだ存在するノードは、削除済みとしてマークされます。

ノードが移動したかどうかを判断するにはどうすればよいですか?

ここには、ノードが移動したかどうかを判断するための変数lastPlacedIndexがあります。この値は、ノードが新しい Fiber リンク リストに追加されるたびに更新されます。

再利用されたノードのoldIndexlastPlacedIndexより小さい場合、そのノードは移動されます。移動する必要がない場合は、 lastPlacedIndexより大きなoldIndexに更新され、次のノードは新しい値によって判断されます。コードは次のとおりです。

関数 placeChild(
  newFiber: ファイバー、
  lastPlacedIndex: 数値、
  新しいインデックス: 数値、
): 番号 {
  新しいファイバーのインデックスを作成します。
  現在の定数を newFiber.alternate に設定します。
  (現在の値 !== null) の場合 {
    const oldIndex = 現在のインデックス;
    (古いインデックスが最後の配置インデックスより小さい場合){
 			// ノードの移動 newFiber.flags = Placement;
      lastPlacedIndex を返します。
    } それ以外 {
      // ノードの位置は変更されていません return oldIndex;
    }
  } それ以外 {
    // 新しいノードを挿入します newFiber.flags = Placement;
    lastPlacedIndex を返します。
  }
}

例えば:

// 古いabcd
// 新しいacbd

abcd はすべてキー値です。

最初の走査ラウンドの後、残りのノードを比較する必要があります。

// 古いbcd
// 新しい CBD

ノード a は最初のラウンドで再利用され、placeChild が呼び出されました。この時点で、lastPlacedIndex 値は 0 です。

2 回目のトラバーサルに入ると、新しいノードは依然としてトラバーサル オブジェクトです。

c => 古いノードに存在し、再利用できます。古いノードのインデックスは 2 です。2 > lastPlacedIndex(0)。移動する必要はありません。lastPlacedIndex に 2 を割り当てます。
b => は古いノードに存在し、再利用できます。古いノードでのインデックスは 1 で、1 < lastPlacedIndex(2) です。移動して Placement でマークする必要があります。
d => 古いノードに存在し、再利用できます。古いノードのインデックスは3で、3 > lastPlacedIndex(2)なので、移動する必要はありません。

この例から、React では、移動する必要のない右側のノードが参照として使用され、移動する必要があるノードはすべて左から右に均一に移動されることがわかります。

後続のレイアウト ステージでは、 PlacementでマークされたノードがinsertBeforeされます。

要約する

React の Diff アルゴリズムのコア コードはそれほど長くありませんが、キーの導入により複雑度が O(n3) から O(n) に巧妙に変更されます。

プログラマー間の社内競争が激しいので、ソースコードを学ばなければなりません。

上記は、React diffアルゴリズムのソースコード分析の詳細な内容です。React diffアルゴリズムの詳細については、123WORDPRESS.COMの他の関連記事に注目してください。

以下もご興味があるかもしれません:
  • React の Diff アルゴリズムをご存知ですか?
  • Reactのdiffアルゴリズムの詳細な分析
  • Reactの仮想DOMとdiffアルゴリズムの詳細な説明
  • ReactレンダリングプロセスからのDiffアルゴリズムの分析に関する簡単な説明
  • React diffアルゴリズムの実装例
  • React diff アルゴリズムの実装アイデアと原理分析

<<:  Linuxでのaliasコマンドの使い方の詳細な説明

>>:  「MySQL サービスを開始できません エラー 1069」を解決する方法

推薦する

CentOS 7 で rpm パッケージを使用して MySQL 5.7.18 をインストールする

最近、MySQL を使っています。Linux での mysql-installation という記事...

CSS の Flex レイアウトを使用してシンプルな縦棒グラフを作成する方法

以下は、Flex レイアウトを使用した棒グラフです。 HTML: <div class=&qu...

Centos7 システムに k8s クラスターを展開するための詳細な紹介

目次1 バージョンと計画1.1 バージョン情報: 1.2 クラスター計画2. 展開1. ファイアウォ...

HTML 基本要約推奨事項 (テキスト形式)

HTMLテキスト書式タグ 標簽 描述 <b> 定義粗體文本 <em> 呈現...

JavaScript をスリープまたは待機させる方法

目次概要setTimeout() の確認スリープ関数の書き方シンプルな選択ループで実行されますか?要...

VueはExcelデータをエクスポートするパブリック関数メソッドをカプセル化します

vue+element UI は Excel データをエクスポートするためのパブリック関数をカプセル...

ApacheとTomcatによるクラスタ環境構築プロセスの分析

実際、Apacheクラスタを構築するのは難しくありません。私もインターネットで情報を見つけて自分で設...

JavaScript で 24 以上の配列メソッドを手動で実装する

目次1. トラバーサルクラス1. 各2. 地図3. すべての4. いくつか5. フィルター6. 減ら...

CSS ですべての子要素を選択し、スタイルを追加する方法

方法:実際のプロジェクトを例に挙げてみましょう。 .lk-ツールバー{ .el-入力{ 幅: 169...

複数の Docker コンテナが同じポート番号を持たない場合の解決策

背景Dockerでは、同じイメージを使用して4つのコンテナを作成します。ネットワークはブリッジモード...

MySQL FAQ シリーズ: 一時テーブルを使用する場合

一時テーブルの概要一時テーブルとは: MySQL は中間結果セットを保存するために使用されます。一時...

ブラインドの特殊効果を実現するネイティブJS

この記事では、ネイティブ JS で実装されたブラインドの特殊効果を紹介します。効果は次のとおりです。...

MySQL開発標準と使用スキルの概要

1. 命名規則1. データベース名、テーブル名、フィールド名には小文字を使用し、アンダースコアで区切...

HTML テーブルタグチュートリアル (21): 行の境界線の色属性 BORDERCOLOR

テーブルを美しくするために、行ごとに異なる境界線の色を設定できます。基本的な構文<TR 境界線...

よくある HTML タグの記述エラー

HTML Police がコードを調べて意味のないタグをすべて見つけ出すので、注意を払う必要がありま...