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 コアは、優先順位に基づいてサイクリックなタスクを実装することです。ただし、この深さのトラバーサルの最大の問題は、大きなDOM をdiff + patch Mount ことで、大きなジャムを引き起こします。 React16 のVDOM に到達した場合、 requestIdleCallback /更新DOM (再帰的diff をFiber タスクに分割Fiber 、 React16 DOM Fiber 小さな部分をチェックし、 16 のVDOM を継続し、忙しくない場合に続行する時間があるかどうかを確認します。 Fiber diff ステージで以下の操作を実行しますが、これは実際には15 のdiff アルゴリズムステージで優先タスクスケジューリング制御を実行することと同等です。 - 中断可能な作業を小さなタスクに分割します。
- 進行中の作業の優先度を調整し、やり直し、以前の(未完了の)作業を再利用します。
-
diff フェーズでのタスク スケジューリングの優先度制御。
仮想DOMの操作とネイティブDOMの操作の比較ここではYoudaさんの言葉をそのまま引用します(回答は2016-02-08 のもので、 Vue2.0 まだリリースされていませんでしたVue2.0 は2016-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, Knockout 、 Vue 、 Avalon などの他の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 shouldComponentUpdate やimmutable 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 の関連部分については詳しく紹介しません。 beginWork 各Fiber を 1 つずつdiff 、次のFiber を比較するたびに、まずこのフレームの残り時間が十分かどうかを判断するため、期間は中断可能であることに注意してください。 16 より前のリンク リストの各ノードはFiber であり、仮想DOM ノードではありません。 各Fiber には、リンク リストの先頭から比較を開始するアルゴリズムを使用するReact16 diff 戦略があります。 これはチェーンのような深さ優先トラバーサル、つまりツリー構造からリンク リスト構造に変わったもので、実際には15 のdiff アルゴリズム ステージでの優先タスク スケジューリング制御に相当します。また、各Fiber には、 child 、 sibling 、 return リンク ツリーの前後へのポインターとして機能します。 child 、ツリー構造をシミュレートする構造ポインターです。 effectTag 、 effect の種類を記録するために使用される非常に興味深いタグです。 effect 、変更、削除、その他の操作など、 DOM を操作する方法を指し、後でcommit ために使用されます (データベースに似ています)。 firstEffect 、 lastEffect などは、中断前後のeffect の状態を保存するために使用され、ユーザーは中断後に以前の操作を復元できます。 tag マーキングに使用されます。 reconcileChildren 広く普及しているVirtul DOM diff 実装していますが、これは実際には単なるエントリ関数です。最初のレンダリングの場合はcurrent がnull になり、 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 = 調整子ファイバー(
進行中、
現在の子、
次に子供たち、
レンダリング有効期限、
);
}
}
mountChildFibers とreconcileChildFibers の違いを比較すると、どちらも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 を返します。
} reconcileChildFibers 、 diff 部分のメイン コードです。関連する操作はすべてChildReconciler 関数にあります。この関数の関連パラメーターは次のとおりです。 returnFiber はdiff の対象となるレイヤーの親ノード、 currentFirstChild は現在のレイヤーの最初のFiber ノード、 newChild は更新されるvdom ノード ( TextNode 、 ReactElement 、または配列の場合があります) であり、 Fiber ノードではありません。 expirationTime 有効期限です。このパラメータはスケジュールに関係しており、 diff とはあまり関係がありません。また、 reconcileChildFibers reconcile(diff) のレイヤー構造であることに注意してください。 まず、最も単純なTextNode diff を見てみましょう。 diff TextNode には 2 つのケースがあります。 -
currentFirstNode はTextNode です。 -
currentFirstNode はTextNode ではありません。
ノードの再利用には 2 つのケースがあります。最初のケースでは、 xxx はTextNode であり、このノードは再利用できることを意味します。再利用されたノードはパフォーマンスの最適化に非常に役立ちます。新しいchild にはTextNode が 1 つしかないため、ノードを再利用した後、残りのaaa ノードを削除し、 div のchild workInProgress に追加できます。 useFiber はノードを再利用するためのメソッドで、 deleteRemainingChildren 残りのノードを削除するメソッドです。ここでは、 currentFirstChild.sibling から削除を開始します。
(currentFirstChild !== null && currentFirstChild.tag === HostText) の場合 {
// すでにノードが存在するので、それを更新して削除します
// 残り。
deleteRemainingChildren(returnFiber, currentFirstChild.sibling); // 兄弟を削除します。const existing = useFiber(currentFirstChild, textContent, expireTime);
既存の.return = returnFiber;
既存のものを返す; // 再利用 } 2 番目のケースでは、 xxx はTextNode ではないため、このノードは再利用できないため、 currentFirstChild から順に残りのノードが削除されます。 createFiberFromText 、 textContent に基づいてノードを作成するメソッドです。 また、ノードを削除しても、リンクリストからノードが実際に削除されるわけではなく、 delete tag が追加されるだけで、 commit が行われると実際に削除されます。
// 既存の最初の子はテキストノードではないので、作成する必要があります
// 既存のものを削除します。
// 新しい Fiber ノードを作成し、古いノードとその兄弟を削除します deleteRemainingChildren(returnFiber, currentFirstChild);
const 作成 = createFiberFromText(
テキストコンテンツ、
ファイバーモードを返す、
有効期限、
); 次はReact Element のdiff です。このとき、ノードの親ノードがこのノードのみであるという状況を扱っています。上記のTextNode のdiff と同様に、考え方は同じです。まず、再利用できるノードがあるかどうかを調べます。ない場合は、別のノードを作成します。このとき、上記 2 つの仮定に基づいて、ノードが再利用可能かどうか、つまりkey が同じかどうか、ノード タイプが同じかどうかが判断されます。上記が同じであれば、ノードは内容が変更されただけで、新しいノードを作成する必要がないため、再利用可能であると考えられます。ノードが異なるタイプの場合は、現在のノードから残りのノードを削除します。再利用可能なノードを検索する際には、最初のノードが再利用可能かどうかに着目するだけではなく、レイヤー内をループし続け、再利用可能なノードを探します。最上位のwhile と最下位のchild = child.sibling; は、子ノードから同じkey とtag 持つ再利用可能なノードを探し続けるためのものです。また、ノードを削除しても、リンクリストから実際にノードが削除されるわけではなく、 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.0 、 patch ときに両端比較方式を使用して直接実装されています。 まず、同じ位置を比較することを考えてみましょう。これは比較的簡単に考えることができる方法です。つまり、 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 メソッドは、再利用できるかどうかを定義します。テキスト ノードの場合、 key がnull でない場合、古いノードは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 を返します。
} newChild がObject の場合、 while がない点を除けば、基本的にReactElement のdiff と同様です。 key と要素の型が等しいかどうかを判定して、再利用できるかどうかを判定します。まず、オブジェクトかどうかを判別するために、 typeof newChild === object && newChild!== null を使用します。 typeof null もobject なので!== null を追加する必要があることに注意してください。次に、 $$typeof を使用して、 REACT_ELEMENT_TYPE かREACT_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 内のすべてのアレイ要素を配置し、新しい配列を通過し、新しいアレイにkey をindex ている場合、 Map はkey にkey に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 またはindex でMap に入れ、新しい配列を繰り返し、これが動きの場合です。
毎日の質問 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アルゴリズムの実装のアイデアと原則分析
|