Reactのdiffアルゴリズムの詳細な分析

Reactのdiffアルゴリズムの詳細な分析

Reactのdiffアルゴリズムの理解

diffアルゴリズムは、 Virtual DOMの変更された部分を計算し、ページ全体を再レンダリングせずにこの部分に対してDOM操作を実行するために使用されます。 DOM構造全体をレンダリングするプロセスは非常にコストがかかり、ブラウザはDOM構造を再描画してリフローする必要があります。 diffアルゴリズムは、操作中にDOM全体を更新せずに、 DOM構造の変更された部分のみを更新できます。 これにより、 DOM構造の操作を最小限に抑え、ブラウザの再描画とリフローの規模を最小限に抑えることができます。

仮想DOM

diffアルゴリズムの基礎はVirtual DOMです。 Virtual DOM JavaScriptオブジェクトに基づくツリーです。React React 、通常JSXを通じてコン​​パイルされます。各ノードはVNodeと呼ばれ、ノードはオブジェクトのプロパティによって記述されます。実際、これは実際のDOMの抽象化です。最終的には、このツリーはレンダリング操作を通じて実際の環境にマッピングできます。簡単に言えば、 Virtual DOMドキュメント全体を記述するJsオブジェクトです。
ブラウザでページを構築するときは、 DOMノードを使用してドキュメント全体を記述する必要があります。

<div class="root" name="root">
    <p>1</p>
    <div>11</div>
</div>

上記のノードとドキュメントをJsオブジェクトで記述すると、次のようになります。もちろん、これはReactでノードを記述するために使用するオブジェクトではありません。React でReact要素を作成するためのソースコードはReact react/src/ReactElement.jsにあります。この記事のReactバージョンは16.10.2です。

{
    タイプ: "div",
    小道具: {
        クラス名: "ルート"
        名前: "ルート",
        子供たち: [{
            タイプ: "p",
            小道具: {
                子供たち: [{
                    タイプ: "テキスト",
                    小道具: {
                        テキスト 1"
                    }
                    
                }]
            }    
        },{
            タイプ: "div",
            小道具: {
                子供たち: [{
                    タイプ: "テキスト",
                    小道具: {
                        テキスト: "11"
                    }
                }]
            }
        }]
    }
}

実際、 Fiber Fiberコアは、優先順位に基づいてサイクリックなタスクを実装することです。ただし、この深さのトラバーサルの最大の問題は、大きなDOMdiff + patch Mountことで、大きなジャムを引き起こします。 React16VDOMに到達した場合、 requestIdleCallback /更新DOM (再帰的diffFiberタスクに分割FiberReact16 DOM Fiber小さな部分をチェックし、 16VDOMを継続し、忙しくない場合に続行する時間があるかどうかを確認します。 Fiber diffステージで以下の操作を実行しますが、これは実際には15diffアルゴリズムステージで優先タスクスケジューリング制御を実行することと同等です。

  • 中断可能な作業を小さなタスクに分割します。
  • 進行中の作業の優先度を調整し、やり直し、以前の(未完了の)作業を再利用します。
  • diffフェーズでのタスク スケジューリングの優先度制御。

仮想DOMの操作とネイティブDOMの操作の比較

ここではYoudaさんの言葉をそのまま引用します(回答は2016-02-08のもので、 Vue2.0まだリリースされていませんでしたVue2.02016-10-01頃にリリースされ、 Vue2.0仮想DOMが追加されました)。関連リンクはhttps://www.zhihu.com/question/31809713です。リンク先の質問と合わせて読むことをお勧めします。また、質問中の比較例も見ることができます。また、以下の回答も非常に重要です。

ネイティブDOM操作とフレームワークによってカプセル化された操作

これは、パフォーマンスvs保守性のトレードオフです。フレームワークの目的は、基礎となるDOM操作を非表示にして、より宣言的な方法で目標を記述できるようにすることで、コードの保守が容易になるようにすることです。フレームワークのDOM操作層は、上位レベルのAPIによって生成される可能性のあるすべての操作を処理する必要があり、その実装は普遍的でなければならないため、 DOM操作の純粋な手動最適化よりも高速なフレームワークはありません。どのbenchmarkでも、どのフレームワークよりも高速な手動最適化を作成できますが、その意味は何でしょうか。実際のアプリケーションを構築するときに、すべての場所で手動最適化を実行しますか。保守性の観点から、これは明らかに不可能です。フレームワークによって提供される保証は、手動最適化を必要とせずに適切なパフォーマンスを提供できることです。

React の仮想 DOM に関する誤解

ReactネイティブDOM操作より速いとは決して言っていませReact 。React React基本的な思考モードは、変更があるたびにアプリケーション全体を再レンダリングすることです。Virtual Virtual DOMがない場合、 innerHTML直接リセットすると考えるのは簡単です。多くの人は、すべてのデータが変更された大きなリストの場合、 innerHTMLをリセットすることは実際には合理的な操作であることを認識していません。本当の問題は、すべてを再レンダリングするという思考モードの下では、1行のデータが変更しただけでも、 innerHTML全体をリセットする必要があり、明らかに多くの無駄があることです。
innerHTML vs Virtual DOMの再描画パフォーマンスの消費を比較することができます。

  • innerHTML : render html string O(template size) + すべてのDOM要素を再作成O(DOM size)
  • Virtual DOM : render Virtual DOM + diff O(template size) + 必要なDOM更新O(DOM change)

Virtual DOM render + diffは、 html列のレンダリングよりも明らかに遅くなりますが、それでも純粋なjs計算であり、後続のDOM操作よりもはるかに安価です。 innerHTMLの計算全体は、 js計算であれDOM操作であれ、インターフェイス全体のサイズに関係しますが、 Virtual DOMの計算のうち、インターフェイスのサイズに関係するのはjs計算のみであり、 DOM操作はデータ変更の量に関係することがわかります。 前述のように、 DOM操作と比較して、 js計算は非常に安価です。これがVirtual DOM: 1)データがどれだけ変更されても、各再描画のパフォーマンスが許容範囲内であることを保証します。2 2) innerHTMLと同様のアイデアを使用してアプリケーションを作成できます。

MVVM と仮想 DOM

Reactと比較して、 Angular, KnockoutVueAvalonなどの他のMVVMフレームワークはすべてデータバインディングを使用します:つまり、 Directive/Bindingオブジェクトを通じてデータの変更を監視し、実際のDOM要素への参照を保持し、データの変更があった場合に対応する操作を実行します。 MVVMの変更検出はデータレベルで行われますが、 ReactのチェックはDOM構造レベルで行われます。 MVVMのパフォーマンスは、変更検出の実装原則によっても異なります。 Angularのダーティチェックでは、変更ごとに固定のO(watcher count)コストがかかります。 Knockout/Vue/Avalonすべて依存関係コレクションを使用しており、 jsレベルとDOMレベルの両方でO(change)です。

  • ダーティチェック: scope digest O(watcher count) + 必要なDOM更新O(DOM change)
  • 依存関係の収集: 依存関係の再収集O(data change) + 必要なDOM O(DOM change)

ご覧のとおり、 Angularの最も非効率的な側面は、小さな変更でもwatcherの数に関連するパフォーマンス コストが発生することです。ただし、すべてのデータが変更された場合、 Angular実際には不利ではありません。依存関係のコレクションは、初期化中およびデータが変更されたときに再収集する必要があります。このコストは、更新が少量の場合はほとんど無視できますが、データ量が膨大な場合は、一定の消費が発生します。
MVVMがリストをレンダリングする場合、各行には独自のデータ スコープがあるため、各行には通常、対応するViewModelインスタンス、またはプロトタイプ継承を使用する少し軽量なscopeオブジェクトがありますが、これにも一定のコストがかかるため、 MVVMリスト レンダリングの初期化はReactよりも確実に遅くなります。これは、 ViewModel / scopeインスタンスの作成がVirtual DOMよりもはるかにコストがかかるためです。ここでのすべてのMVVM実装に共通する問題は、リスト レンダリングのデータ ソースが変更された場合、特にデータがまったく新しいオブジェクトである場合に、作成されたViewModelインスタンスとDOM要素を効果的に再利用する方法です。再利用の最適化がない場合、データがまったく新しいため、 MVVM実際には以前のインスタンスをすべて破棄し、すべてのインスタンスを再作成し、最後に再度レンダリングする必要があります。これが、タイトルにリンクされているangular/knockout実装が比較的遅い理由です。対照的に、 Reactの変更チェックはDOM構造レベルで行われるため、まったく新しいデータであっても、最終的なレンダリング結果が変わらない限り、無駄な作業を行う必要はありません。
ちなみに、 Reactリストをレンダリングするときは、 keyと呼ばれる特別なpropも提供する必要があります。これは基本的にtrack-byと同じです。

パフォーマンスの比較は状況によっても異なります

パフォーマンスを比較する場合、初期レンダリング、小さなデータ更新、大きなデータ更新などのさまざまなシナリオを区別することが重要です。 Virtual DOM 、ダーティ チェックMVVM 、データ収集MVVM 、シナリオによってパフォーマンスが異なり、最適化の要件も異なります。小さなデータ更新のパフォーマンスを向上させるために、 Virtual DOM shouldComponentUpdateimmutable dataなどのターゲットを絞った最適化も必要です。

  • 初期レンダリング: Virtual DOM > ダーティ チェック >= 依存関係のコレクション。
  • 小さなデータ更新: 依存関係の収集 >> Virtual DOM + 最適化 > ダーティ チェック (最適化できません) > 最適化なしのVirtual DOM
  • 大規模なデータ更新: ダーティ チェック + 最適化 >= 依存関係の収集 + 最適化 > Virtual DOM (最適化できない/最適化する必要がない) >> 最適化なしのMVVM

Virtual DOMが高速だと思い込まないでください。Diff diff無料ではなく、 batching MVVMで実行でき、最終的なpatchにはネイティブAPIを使用する必要があります。私の意見では、 Virtual DOMの真の価値はパフォーマンスではなく、 1)機能的なUIプログラミングへの扉を開くこと、 2) ReactNativeなどのDOM以外のbackendにレンダリングできることです。

要約する

上記の比較は、主にフレームワーク開発者や研究者に参考資料を提供するためのものです。主流のフレームワーク+合理的な最適化は、ほとんどのアプリケーションのパフォーマンス要件を満たすのに十分です。極端なパフォーマンス要件のある特別なケースがある場合は、手動の最適化のためにメンテナンス性をある程度犠牲にする必要があります:たとえば、 Atomエディターは、ファイルレンダリングの実装でReactを放棄し、独自のtile-based renderingを採用しました。たとえば、モバイル端末では、 DOM-pooling仮想スクロールが必要であり、順序の変更を考慮する必要がないため、フレームワークの組み込み実装をバイパスして自分で作成できます。

差分アルゴリズム

Reactメモリ内に仮想DOMツリーを保持します。データ ( state & props ) が変更されると、仮想DOM自動的に更新して新しい仮想DOMツリーを取得します。次に、 Diffアルゴリズムを使用して、古い仮想 DOM ツリーと新しい仮想DOMツリーを比較して、変更された部分が最も小さい部分を見つけ、変更された部分のPatchキューに追加し、最後にこれらのPatchバッチで実際のDOMに更新します。

時間計算量

まず、完全なdiffにはO(n^3)時間計算量が必要で、これは最小編集距離の問題です。文字列の最小編集距離を比較すると、動的計画法を使用した場合の時間計算量はO(mn)です。ただし、 DOMはツリー構造であり、ツリー構造の最小編集距離問題の時間計算量は30年以上の進化でO(m^3n^3)からO(n^3)に進化しています。この問題に興味がある場合は、論文https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdfを研究することができます。
もともと効率化のために導入されたdiffアルゴリズムの場合、時間計算量O(n^3)を使用するのは明らかに適切ではありません。ノード要素が1000ある場合、10 億回の比較が必要になります。これは高価なアルゴリズムであるため、プロセスを高速化するために何らかの妥協が必要です。いくつかの戦略を通じて比較を簡素化し、時間計算量をO(n)に削減します。これは最小編集距離ではありませんが、編集距離と時間パフォーマンスを総合的に考慮すると、より良いソリューションであり、より良い妥協点です。

異なる戦略

上で述べたO(n)時間計算量は、特定の戦略によって達成されます。React Reactドキュメントには、次の 2 つの仮定が記載されています。

  • 異なるタイプの 2 つの要素によって、異なるツリーが生成されます。
  • レンダラーにkey属性を添付することで、開発者はどの子要素が安定しているかを示すことができます。

簡単に言えば:

  • 同じレベルでの比較のみが実行され、レベル間の移動は作成および削除操作と見なされます。
  • 要素の型が異なる場合は、新しい要素が作成されたとみなされ、その子は再帰的に比較されません。
  • リスト要素などのコンテンツが類似している場合は、 key使用して、移動、作成、または削除操作であるかどうかを一意に判断できます。

比較後、いくつかの状況が発生し、対応する操作が実行されます。

  • このノードは追加または削除されます->新しいノードを追加または削除します。
  • 属性が変更されます->古い属性が新しい属性に変更されます。
  • テキストコンテンツが変更されます->古いコンテンツが新しいコンテンツに変更されます。
  • ノードtagまたはkeyが変更されたかどうか->変更された場合は削除して新しい要素を作成します。

分析する

分析では補助的な役割を果たすReactのソースコードを簡単に引用します。実際のソースコードは非常に複雑なので、理解を助けるためにいくつかの断片を引用します。この記事のソースコードTAG 16.10.2です。
if (__DEV__){...}に関連するコードは、実際には開発者エクスペリエンスを向上させるために書かれています。わかりやすいエラー報告、 renderパフォーマンステスト、およびReactのその他のコードはすべてif (__DEV__)で書かれています。これらのコードはproduction build中にパッケージ化されないため、開発者専用のコードを安心して提供できます。React Reactベストプラクティスの 1 つは、開発中はdevelopment buildを使用し、プロダクション環境ではプロダクションproduction buildを使用することです。そのため、実際にはこの部分のコードをスキップして、よりコアな部分の理解に集中できます。
diffアルゴリズムの分析はreconcileChildrenから始まります。 setState -> enqueueSetState(UpdateQueue) -> scheduleUpdate -> performWork -> workLoop -> beginWork -> finishClassComponent -> reconcileChildrenの関連部分については詳しく紹介しません。 beginWorkFiberを 1 つずつdiff 、次のFiberを比較するたびに、まずこのフレームの残り時間が十分かどうかを判断するため、期間は中断可能であることに注意してください。 16より前のリンク リストの各ノードはFiberであり、仮想DOMノードではありません。 各Fiberには、リンク リストの先頭から比較を開始するアルゴリズムを使用するReact16 diff戦略があります。 これはチェーンのような深さ優先トラバーサル、つまりツリー構造からリンク リスト構造に変わったもので、実際には15diffアルゴリズム ステージでの優先タスク スケジューリング制御に相当します。また、各Fiberには、 childsiblingreturnリンク ツリーの前後へのポインターとして機能します。 child 、ツリー構造をシミュレートする構造ポインターです。 effectTageffectの種類を記録するために使用される非常に興味深いタグです。 effect 、変更、削除、その他の操作など、 DOMを操作する方法を指し、後でcommitために使用されます (データベースに似ています)。 firstEffectlastEffectなどは、中断前後のeffectの状態を保存するために使用され、ユーザーは中断後に以前の操作を復元できます。 tagマーキングに使用されます。
reconcileChildren広く普及しているVirtul DOM diff実装していますが、これは実際には単なるエントリ関数です。最初のレンダリングの場合はcurrentnullになり、 mountChildFibersを通じて子ノードのFiberインスタンスが作成されます。最初のレンダリングでない場合はreconcileChildFibersを呼び出してdiffを行い、その後effect listを取得します。

// react-reconciler/src/ReactChildFiber.js 行 1246
エクスポート関数 reconcileChildren(
  現在の: ファイバー | null、
  進行中: ファイバー、
  次の子供: 任意、
  renderExpirationTime: 有効期限、
){
  if (current === null) { // 最初のレンダリングで子ノードの `Fiber` インスタンスを作成します workInProgress.child = mountChildFibers(
      進行中、
      ヌル、
      次に子供たち、
      レンダリング有効期限、
    );
  } else { // それ以外の場合は `reconcileChildFibers` を呼び出して `diff` を実行します
    workInProgress.child = 調整子ファイバー(
      進行中、
      現在の子、
      次に子供たち、
      レンダリング有効期限、
    );
  }
}

mountChildFibersreconcileChildFibersの違いを比較すると、どちらもChildReconcilerファクトリー関数から取得されますが、渡されるパラメーターが異なります。このパラメーターはshouldTrackSideEffectsと呼ばれます。その機能は、 effectTagを追加するかどうかを決定することです。初期レンダリングでは更新操作がないため、主に初期レンダリングを最適化するために使用されます。 ChildReconciler 、内部に多くのhelper関数を含む非常に長いファクトリー (ラッパー) 関数です。最終的に返される関数はreconcileChildFibersと呼ばれ、子fiberノードのreconciliationを実装します。

// react-reconciler/src/ReactChildFiber.js 行 1370
エクスポート const reconcileChildFibers = ChildReconciler(true);
エクスポート const mountChildFibers = ChildReconciler(false);

関数 ChildReconciler(shouldTrackSideEffects) { 
  // ...
  関数deleteChild(){
      // ...
  }
  関数useFiber(){
      // ...
  }
  関数 placeChild(){
      // ...
  }
  関数 placeSingleChild(){
      // ...
  }
  関数 updateTextNode(){
      // ...
  }
  関数 updateElement(){
      // ...
  }
  関数 updatePortal(){
      // ...
  }
  関数 updateFragment(){
      // ...
  }
  関数createChild(){
      // ...
  }
  関数 updateSlot(){
      // ...
  }
  関数 updateFromMap(){
      // ...
  }
  関数 warnOnInvalidKey(){
      // ...
  }
  関数 reconcileChildrenArray(){
      // ...
  }
  関数 reconcileChildrenIterator(){
      // ...
  }
  関数 reconcileSingleTextNode(){
      // ...
  }
  関数 reconcileSingleElement(){
      // ...
  }
  関数 reconcileSinglePortal(){
      // ...
  }
  関数 reconcileChildFibers(){
      // ...
  }
  reconcileChildFibers を返します。
}

reconcileChildFibersdiff部分のメイン コードです。関連する操作はすべてChildReconciler関数にあります。この関数の関連パラメーターは次のとおりです。 returnFiberdiffの対象となるレイヤーの親ノード、 currentFirstChildは現在のレイヤーの最初のFiberノード、 newChildは更新されるvdomノード ( TextNodeReactElement 、または配列の場合があります) であり、 Fiberノードではありません。 expirationTime有効期限です。このパラメータはスケジュールに関係しており、 diffとはあまり関係がありません。また、 reconcileChildFibers reconcile(diff)のレイヤー構造であることに注意してください。

まず、最も単純なTextNode diffを見てみましょう。 diff TextNodeには 2 つのケースがあります。

  • currentFirstNodeTextNodeです。
  • currentFirstNodeTextNodeではありません。

ノードの再利用には 2 つのケースがあります。最初のケースでは、 xxxTextNodeであり、このノードは再利用できることを意味します。再利用されたノードはパフォーマンスの最適化に非常に役立ちます。新しいchildにはTextNodeが 1 つしかないため、ノードを再利用した後、残りのaaaノードを削除し、 divchild workInProgressに追加できます。 useFiberはノードを再利用するためのメソッドで、 deleteRemainingChildren残りのノードを削除するメソッドです。ここでは、 currentFirstChild.siblingから削除を開始します。

(currentFirstChild !== null && currentFirstChild.tag === HostText) の場合 {
  // すでにノードが存在するので、それを更新して削除します
  // 残り。
  deleteRemainingChildren(returnFiber, currentFirstChild.sibling); // 兄弟を削除します。const existing = useFiber(currentFirstChild, textContent, expireTime);
  既存の.return = returnFiber;
  既存のものを返す; // 再利用 }

2 番目のケースでは、 xxxTextNodeではないため、このノードは再利用できないため、 currentFirstChildから順に残りのノードが削除されます。 createFiberFromTexttextContentに基づいてノードを作成するメソッドです。 また、ノードを削除しても、リンクリストからノードが実際に削除されるわけではなく、 delete tagが追加されるだけで、 commitが行われると実際に削除されます。

// 既存の最初の子はテキストノードではないので、作成する必要があります
// 既存のものを削除します。
// 新しい Fiber ノードを作成し、古いノードとその兄弟を削除します deleteRemainingChildren(returnFiber, currentFirstChild);
const 作成 = createFiberFromText(
  テキストコンテンツ、
  ファイバーモードを返す、
  有効期限、
);

次はReact Elementdiffです。このとき、ノードの親ノードがこのノードのみであるという状況を扱っています。上記のTextNodediffと同様に、考え方は同じです。まず、再利用できるノードがあるかどうかを調べます。ない場合は、別のノードを作成します。このとき、上記 2 つの仮定に基づいて、ノードが再利用可能かどうか、つまりkeyが同じかどうか、ノード タイプが同じかどうかが判断されます。上記が同じであれば、ノードは内容が変更されただけで、新しいノードを作成する必要がないため、再利用可能であると考えられます。ノードが異なるタイプの場合は、現在のノードから残りのノードを削除します。再利用可能なノードを検索する際には、最初のノードが再利用可能かどうかに着目するだけではなく、レイヤー内をループし続け、再利用可能なノードを探します。最上位のwhileと最下位のchild = child.sibling;は、子ノードから同じkeytag持つ再利用可能なノードを探し続けるためのものです。また、ノードを削除しても、リンクリストから実際にノードが削除されるわけではなく、 delete tagが追加されるだけで、 commitが行われると実際に削除されます。

// react-reconciler/src/ReactChildFiber.js 行 1132
定数キー = 要素.キー;
子を現在の最初の子とします。
while (child !== null) {
  // TODO: key === null かつ child.key === null の場合、これは次の場合にのみ適用されます。
  // リストの最初の項目。
  if (child.key === key) {
    もし (
      child.tag === フラグメント
        ? 要素タイプ === REACT_FRAGMENT_TYPE
        : child.elementType === element.type ||
          // このチェックをインラインで保持して、偽のパスでのみ実行されるようにします。
          (__DEV__
            ? iscompatibleFamilyForHotReloading(子、要素)
            : 間違い)
    ){
      deleteRemainingChildren(returnFiber, child.sibling); // 現在のノードにはノードが 1 つしかなく、古いノードには削除する必要がある兄弟ノードがあるため、冗長です。const existing = useFiber(
        子供、
        要素タイプ === REACT_FRAGMENT_TYPE
          ? 要素.props.children
          : 要素.props、
        有効期限、
      );
      existing.ref = coerceRef(returnFiber、child、element);
      既存の.return = returnFiber;
      // ...
      既存のものを返します。
    } それ以外 {
      残りの子供を削除します(returnFiber、子供)。
      壊す;
    }
  } それ以外 {
    deleteChild(returnFiber, child); // 子から削除を開始する
  }
  child = child.sibling; // 子ノードから再利用可能なノードを探し続けます}

次のステップは、再利用可能なノードが見つからないため、ノードを作成することです。Fragment Fragmentの作成方法は、一般的なElementノードの作成方法とは異なります。これは、 Fragmentが元々意味のないノードであるためです。実際に作成する必要があるFiber 、それ自体ではなく、そのchildrenです。したがって、 createFiberFromFragment elementではなくelement.props.childrenを渡します。

// react-reconciler/src/ReactChildFiber.js 行 1178
要素タイプ === REACT_FRAGMENT_TYPE の場合 {
  const 作成 = createFiberFromFragment(
    要素.props.children、
    ファイバーモードを返す、
    有効期限、
    要素.キー、
  );
  作成された戻り値 = returnFiber;
  戻り値が作成されました。
} それ以外 {
  const 作成された = createFiberFromElement(
    要素、
    ファイバーモードを返す、
    有効期限、
  );
  created.ref = coerceRef(returnFiber、currentFirstChild、要素);
  作成された戻り値 = returnFiber;
  戻り値が作成されました。
}

diff Array diffの中で最も複雑な部分であり、多くの最適化が行われています。Fiber Fiberは単一の連結リスト構造であるため、子ノード配列などのデータ構造はなく、両端を同時に比較するための末尾カーソルもありません。そのため、 Reactアルゴリズムは単純化された両端比較方式であり、先頭からのみ比較します。Vue2.0 のdiffアルゴリズムVue2.0patchときに両端比較方式を使用して直接実装されています。
まず、同じ位置を比較することを考えてみましょう。これは比較的簡単に考えることができる方法です。つまり、 diff実行するときに、インデックスに従って新しい配列と古い配列を 1 つずつ比較することができます。再利用できる場合は、古いリンク リストからこのノードを削除します。再利用できない場合は、他の再利用戦略を使用します。現時点では、 newChildren配列はVDOM配列なので、ここではupdateSlotを使用してnewFiberにパッケージ化します。

// react-reconciler/src/ReactChildFiber.js 行 756
関数 reconcileChildrenArray(
    returnFiber: ファイバー、
    currentFirstChild: ファイバー | null、
    新しい子: 配列<*>,
    有効期限: 有効期限、
  ): ファイバー | null {
    // 機械翻訳コメント // ファイバー上にバックポインタがないため、このアルゴリズムは両端で検索して最適化することはできません。このモデルでどこまでできるか見てみたかったんです。トレードオフに価値がないことが判明した場合は、いつでも後で追加し直すことができます。
    // デュアルエンドの最適化でも、変更が少ない場合に最適化し、マップを探すのではなく比較を強制します。前進モードでは、まずそのパスに到達するために探索を行い、かなり先を見る必要があることに気付いた場合にのみマップに移動します。これは、2 つの終了した検索と同様に反転を処理しませんが、これは珍しいことです。さらに、Iterable で両端の最適化を機能させるには、コレクション全体をコピーする必要があります。
    // 最初の反復では、挿入/移動のたびに悪いケース (すべてをマップに追加する) が発生しました。
    // このコードを変更する場合は、同じアルゴリズムを使用する reconcileChildrenIterator() も更新する必要があります。
    oldFiber を currentFirstChild とします。
    lastPlacedIndex = 0 とします。
    newIdx = 0 とします。
    nextOldFiber を null にします。
     // 最初の for ループは、インデックスに従って新しいノードと古いノードを 1 つずつ比較します。新しいノードと古いノードが一致しない場合は、ループを終了し、終了時のノードと oldFiber ノードを記録します for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) { // 位置が一致しません nextOldFiber = oldFiber; // 比較する次の古いノード oldFiber = null; // newFiber も null (再利用できない) の場合、現在の 1 対 1 の比較ループを終了します} else {
        nextOldFiber = oldFiber.sibling; //通常、次のサイクルでは、兄弟ノードの値を取得し、それをoldFiberに割り当てます。
      }
      // // ノードが再利用できる(キー値が一致する)場合は、更新して新しいノードを返します。そうでない場合は、ノードが再利用できないことを示す null を返します。const newFiber = updateSlot( // ノードが再利用できるかどうかを判断します。returnFiber,
        古いファイバー、
        新しい子供[newIdx]、
        有効期限、
      );
      // ノードを再利用してループから抜け出すことはできません if (newFiber === null) { 
        // TODO: これはnullの子のような空のスロットでは機能しません。
        // 残念なことに、常にスローパスがトリガーされます。
        // これがミスかnullかを伝えるより良い方法、
        // ブール値、未定義な​​ど
        (古いファイバーがnullの場合){
          oldFiber = nextOldFiber; // 再利用できないノードを記録して比較を終了します}
        break; // ループを終了する }
      if (shouldTrackSideEffects) {
        // 既存のノードを再利用しない場合は、既存のノードを削除します if (oldFiber && newFiber.alternate === null) {
          // スロットは一致しましたが、既存のファイバーを再利用しなかったため、
          // 既存の子を削除する必要があります。
          子を削除します(returnFiber、oldFiber);
        }
      }
      // このトラバーサルは新しく追加されたノードを挿入済みとしてマークします lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (以前の新しいファイバー === null) {
        // TODO: ループから抜け出します。これは最初の実行時にのみ発生します。
        結果として得られるFirstChild = newFiber;
      } それ以外 {
        // TODO: このスロットの正しいインデックスにない場合は、兄弟を延期します。
        // つまり、以前にnull値があった場合は、これを延期したいのです
        // 各null値に対して。ただし、updateSlotを呼び出すことも望ましくありません。
        // 前のものと一緒。
        前のNewFiber.sibling = newFiber;
      }
      前のNewFiber = 新しいFiber;
      oldFiber = nextOldFiber; // oldFiber を再割り当てしてトラバーサルを続行します}

updateSlotメソッドは、再利用できるかどうかを定義します。テキスト ノードの場合、 keynullでない場合、古いノードはTextNodeではなく、新しいノードはTextNodeであるため、 nullが返され、再利用できません。それ以外の場合は、再利用できます。 updateTextNodeメソッドを呼び出します。 updateTextNodeには、最初のレンダリングのロジックが含まれていることに注意してください。最初のレンダリングでは、 TextNodeは再利用されずに挿入されます。

// react-reconciler/src/ReactChildFiber.js 行 544
関数 updateSlot(
    returnFiber: ファイバー、
    oldFiber: ファイバー | null、
    新しい子: 任意、
    有効期限: 有効期限、
  ): ファイバー | null {
    // キーが一致する場合はファイバーを更新し、それ以外の場合は null を返します。

    const key = oldFiber !== null ? oldFiber.key : null;

    if (typeof newChild === '文字列' || typeof newChild === '数値') {
      // 新しいノードが文字列または数値の場合、キーはありません。
      // 古いノードにキーがある場合は再利用できず、直接 null を返します。
      // 古いノードのキーが null の場合、古いノードはテキストノードであることを意味するため、再利用できます。if (key !== null) {
        null を返します。
      }
      updateTextNode() を返す
        リターンファイバー、
        古いファイバー、
        '' + 新しい子、
        有効期限、
      );
    }

    // ...

    null を返します。
}

newChildObjectの場合、 whileがない点を除けば、基本的にReactElementdiffと同様です。 keyと要素の型が等しいかどうかを判定して、再利用できるかどうかを判定します。まず、オブジェクトかどうかを判別するために、 typeof newChild === object && newChild!== nullを使用します。 typeof nullobjectなので!== nullを追加する必要があることに注意してください。次に、 $$typeofを使用して、 REACT_ELEMENT_TYPEREACT_PORTAL_TYPEかを判断し、それぞれ異なる再利用ロジックを呼び出します。配列もObjectなので、このifには配列の再利用ロジックもあります。

// react-reconciler/src/ReactChildFiber.js 行 569
if (typeof newChild === 'object' && newChild !== null) {
  スイッチ(newChild.$$typeof) {
    case REACT_ELEMENT_TYPE: { // ReactElement 
      if (newChild.key === key) {
        newChild.type === REACT_FRAGMENT_TYPE の場合 {
          updateFragment() を返す
            リターンファイバー、
            古いファイバー、
            新しい子.props.children、
            有効期限、
            鍵、
          );
        }
        updateElement() を返す
          リターンファイバー、
          古いファイバー、
          新しい子、
          有効期限、
        );
      } それ以外 {
        null を返します。
      }
    }
    REACT_PORTAL_TYPEの場合: {
      //updatePortalを呼び出す
      // ...
    }
  }

  if (isArray(newChild) || getIteratorFn(newChild)) {
    (キー !== null) の場合 {
      null を返します。
    }

    updateFragment() を返す
      リターンファイバー、
      古いファイバー、
      新しい子、
      有効期限、
      ヌル、
    );
  }
}

最初のトラバーサルに戻りましょう。トラバーサルが完了すると、古いノードがトラバースされたか、新しいノードがトラバースされたかの 2 つの状況が発生します。この時点で新しいノードをトラバースした場合、つまり更新するものがない場合は、通常、元の配列から要素が削除され、残りの古いノードが削除されることを意味します。最初のサイクルで古いノードが再利用されているが、新しいノードがある場合は、新しいノードが追加された可能性が非常に高くなります。この場合、残っている新しいノードに基づいてFiberを直接作成するだけで済みます。

// react-reconciler/src/ReactChildFiber.js 行 839
//新しいノードが更新され、冗長な古いノードを削除する場合(newIdx === newChildren.length){
  //新しい子供の終わりに到達しました。
  leteremainingchildren(returnfiber、oldfiber);
  結果として結果を返します。
}

//新しいノードが更新され、冗長な古いノードを削除する場合(oldfiber === null){
  //既存の子供がもういない場合は、高速パスを選択できます
  //残りはすべて挿入されるため。
  for(; newidx <newChildren.length; newIdx ++){
    const newfiber = createchild(
      returnfiber、
      NewChildren [NewIdx]、
      expirationtime、
    );
    if(newfiber === null){
      続く;
    }
    lastPlacedIndex = placeChild(newfiber、lastPlaceDindex、newIdx);
    if(forternewfiber === null){
      // TODO:ループから外れます。
      結果firstchild = newFiber;
    } それ以外 {
      以前のnewfiber.sibling = newFiber;
    }
    以前のnewfiber = newFiber;
  }
  結果として結果を返します。
}

次に、動きの場合にノードを再利用する方法、つまり、古い配列と新しい配列の両方がこの要素と位置が異なる場合、 React keyまたはindexに従ってMap内のすべてのアレイ要素を配置し、新しい配列を通過し、新しいアレイにkeyindexている場合、 Mapkeykeyにexedのあるアレイに留めindex keyいます。

// React-Reconciler/src/ReactChildFiber.js行872
//すべての子供をキーマップに追加して、すばやく検索します。
//既存のノードのキーまたはインデックスをOldFiber const const constから始まるマップ構造に追加します= mapRemainingChildren(returnfiber、oldfiber);

//スキャンを続け、マップを使用して、削除されたアイテムを移動として復元します。
//比較されていない残りの新しいノードについては、キーまたはインデックスで古いノードのマップでそれらを1つずつ比較して、それらを再利用できるかどうかを確認します。
for(; newidx <newChildren.length; newIdx ++){
  //主に、新しいノードと古いノードのキーまたはインデックスが同じかどうかを確認し、再利用できるかどうかを確認します。
  const newfiber = updatefrommap(
    既存の子供、
    returnfiber、
    newidx、
    NewChildren [NewIdx]、
    expirationtime、
  );
  if(newFiber!== null){
    if(sextracksideEffects){
      if(newfiber.alternate!== null){
        //新しいファイバーは進行中の作業ですが、存在する場合
        //現在、繊維を再利用する必要があります
        //子リストからそれを削除に追加しないように
        //リスト。
        既存のchildren.delete(//マップ内の再使用ノードのキーまたはインデックスを削除する
          newfiber.key === null?
        );
      }
    }
    lastPlacedIndex = placeChild(newfiber、lastPlaceDindex、newIdx);
    //更新されたNewFiber構造にNewFiberを追加します。
    if(forternewfiber === null){
      結果firstchild = newFiber;
    } それ以外 {
      以前のnewfiber.sibling = newFiber;
    }
    以前のnewfiber = newFiber;
  }
}

// React-Reconciler/src/ReactChildFiber.js行299
//古いノードと古いノードのキーまたはインデックスをマップ構造に保存して、キーまたはインデックス関数MapRemainingChildrenによって取得するのが便利です(
    returnfiber:ファイバー、
    CurrentFirstchild:繊維、
  ):<文字列| fiber> {
    //残りの子供を一時的なマップに追加して、
    //キーはすぐにインデックスでこのセットに追加されます
    // その代わり。
    const既存の子供:map <string | fiber> = new Map();

    既存のチャイルド= currentFirstchild;
    while(既存のチャイルド!== null){
      if(既存のchild.key!== null){
        既存のChildren.set(既存のchild.key、既存のchild);
      } それ以外 {
        既存のChildren.set(既存のchild.index、既存のチャイルド);
      }
      既存のchild = expstinceChild.sibling;
    }
    既存の子供を返します。
  }

この時点で、新しい配列トラバーサルが完了します。つまり、同じレイヤーでのdiffプロセスが完了します。

  • 新しい配列を初めて使用したとき、新しいアレイと古い配列の同じindexを比較し、 updateSlotれたメソッドを介して再利用できるノードを見つけ、再利用できないノードが見つかるまでループを終了します。
  • 最初のトラバーサルが完了した後、残りの古いノードを削除し、新しいノードをトラバースすると、残りの古いノードがバッチで削除され;新しいノードが直接挿入されます。
  • すべての古い配列要素をkeyまたはindexMapに入れ、新しい配列を繰り返し、これが動きの場合です。

毎日の質問

https://github.com/WindrunnerMax/EveryDay

参照する

https://zhuanlan.zhihu.com/p/89363990
https://zhuanlan.zhihu.com/p/137251397
https://github.com/sisteran/blog/issues/22
https://github.com/hujiulong/blog/issues/6
https://juejin.cn/post/6844904165026562056
https://www.cnblogs.com/forcheng/p/13246874.html
https://zh-hans.reactjs.org/docs/reconciliation.html
https://zxc0328.github.io/2017/09/28/react-16-source/
https://blog.csdn.net/halations/article/details/109284050
https://blog.csdn.net/susuzhe123/article/details/107890118
https://github.com/advanced-frontend/daily-interview-question/issues/47
https://github.com/jianjiachenghub/react-deeplearn/blob/Master/%E5%AD%A6%E4%B9%A0%AE%AEC C%8FDIFF%E7%AE%97%E6%B3%95.md

上記は、Reactのdiffアルゴリズムの詳細な分析です。

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

<<:  Linux サーバーに SSH パスワードなしでログインする方法

>>:  イントラネット侵入を実現するためのSSHポート転送

推薦する

Web スライスとは何ですか?

IE8 の新機能 Web スライス (Web スライス) Microsoft は 3 月 20 日...

MySQL ストアド プロシージャの原理と使用法の詳細な説明

この記事では、例を使用して、MySQL ストアド プロシージャの原理と使用方法を説明します。ご参考ま...

Navicatは機能ソリューション共有を作成できません

初めて MySQL FUNCTION を書いたとき、エラーが何度も発生しました。 Err] 1064...

HTML5+CSS3コーディング標準

黄金律プロジェクトに何人の人が取り組んでいるかに関係なく、すべてのコード行が同じ人によって書かれたよ...

CSSクラス名の問題の詳細な説明

数字で始まる次の CSS クラス名は有効になりません。 .1番目{ 色: 赤; }有効な CSS ク...

Vueデータ監視の原理の詳細な説明

<本文> <div id="ルート"> <h1&...

Html+CSS フローティング広告ストリップの実装

1.html部分コードをコピーコードは次のとおりです。 <!DOCTYPE html> ...

Echarts 凡例コンポーネントのプロパティとソース コード

凡例コンポーネントは、ECharts でよく使用されるコンポーネントです。シリーズ マーカーの名前を...

UbuntuでMySQLデータベースファイルディレクトリを変更する方法

序文同社の Ubuntu サーバーは、さまざまなシステムのディレクトリを異なる論理パーティションに配...

Dockerはポートマッピングを設定しますが、ソリューションにアクセスできません

#docker ps チェック、すべてのポートがマップされています コンテナID イメージ コマンド...

JavaScriptの原理と方向性

これが何を指しているのかをどのように判断するのでしょうか? ①グローバル環境で呼び出された場合はwi...

MySQL ベースのストレージエンジンとログの説明 (包括的な説明)

1.1 ストレージエンジンの概要 1.1.1 ファイルシステムストレージファイル システム: オペ...

Ubuntu 18.04 に MySQL をインストールする (グラフィカル チュートリアル)

ヒント: 以下の操作はすべて root 権限で実行されます。 # MySQL がインストールされてい...

CocosCreatorでリストを作成する方法

CocosCreator バージョン: 2.3.4 Cocos には List コンポーネントがない...

イベントバブリング、イベントキャプチャ、イベント委任に基づく詳細な説明

イベントバブリング、イベントキャプチャ、イベント委任JavaScript では、イベント委譲は非常に...