Reactは適応性の高い仮想リストを実装する

Reactは適応性の高い仮想リストを実装する

最近、あるプラットフォームの開発と反復中に、非常に長いリストが antd モーダルにネストされ、読み込みが遅くなり、動作が停止するという状況に遭遇しました。そこで、全体的なエクスペリエンスを最適化するために、仮想スクロール リストをゼロから実装することにしました。

変換前:

変換前に、編集ウィンドウ モーダルを開くと短時間フリーズし、[キャンセル] をクリックして閉じた後、すぐには応答せず、少しためらった後に閉じることがわかります。

変換後:

変換が完了すると、モーダル全体の開き方が以前よりもはるかにスムーズになり、ユーザーのクリックイベントに即座に応答してモーダルを呼び出したり閉じたりできることがわかります。

パフォーマンス比較デモ: codesandbox.io/s/av-list-…

0x0の基本

では、仮想スクロール/リストとは何でしょうか?

仮想リストとは、表示するデータが何千もあるが、ユーザーの「ウィンドウ」(一度に表示されるもの)が大きくない場合に、巧妙な方法を使用して、表示可能な項目の最大数 + 「BufferSize」要素のみをレンダリングし、ユーザーがスクロールしたときに各要素のコンテンツを動的に更新することで、非常に少ないリソースで長いリストのスクロールと同じ効果を実現できることを意味します。

(上の図から、ユーザーが毎回見ることができる実際の要素/コンテンツは、item-4からitem-13までの9つの要素のみであることがわかります)

0x1 「固定高さ」の仮想リストを実装する

まず、いくつかの変数/名前を定義する必要があります。

  • 上の図から、ユーザーの実際の可視領域の開始要素はItem-4であることがわかります。したがって、データ配列内の対応する添え字はstartIndexです。
  • 同様に、Item-13に対応する配列インデックスはendIndexになります。
  • したがって、Item-1、Item-2、Item-3はユーザーの上方向のスワイプ操作によって非表示になるため、これをstartOffset(scrollTop)と呼びます。

表示領域内のコンテンツのみをレンダリングするため、コンテナ全体の動作を長いリスト(スクロール)に似たものに保つには、元のリストの高さを維持する必要があり、HTML構造を次のように設計します。

<!--バージョン 1.0 -->
<div className="vListContainer">
  <div className="ファントムコンテンツ">
    ...
    <!-- 項目-1 -->
    <!-- 項目 2 -->
    <!-- 項目-3 -->
    ....
  </div>
</div>

で:

  • vListContainer は表示領域のコンテナーであり、overflow-y: auto プロパティを持ちます。
  • ファントム内の各データは位置: absoluteでなければなりません
  • PhantomContent は「ファントム」パーツであり、その主な目的は、実際のリストのコンテンツの高さを復元して、長いリストの通常のスクロール動作をシミュレートすることです。

次に、onScroll 応答関数を vListContainer にバインドし、ネイティブ スクロール イベントの scrollTop プロパティに従って関数内の startIndex と endIndex を計算します。

  • 計算を始める前に、いくつかの値を定義する必要があります。

固定リスト要素の高さが必要です: rowHeight
現在のリストにいくつの項目があるかを知る必要があります: 合計
現在のユーザーの可視領域の高さを知る必要があります: height

  • 上記のデータを使用して、次のデータを計算できます。

リストの合計の高さ: phantomHeight = total * rowHeight
表示範囲に表示される要素の数: limit = Math.ceil(height/rowHeight)

したがって、onScroll コールバックで次の計算を実行できます。

onScroll(イベント: 任意) {
  // 応答する必要があるスクロールイベントかどうかを判断します if (evt.target === this.scrollingContainer.current) {
    定数{ scrollTop } = evt.target;
    const { startIndex、 total、 rowHeight、 limit } = this;

    // 現在の開始インデックスを計算する
    const currentStartIndex = Math.floor(scrollTop / rowHeight);

    // currentStartIndex が startIndex と異なる場合 (データを更新する必要があります)
    (現在の開始インデックス !== 開始インデックス ) の場合 {
      this.startIndex = 現在の開始インデックス;
      this.endIndex = Math.min(currentStartIndedx + limit, total - 1);
      this.setState({ scrollTop });
    }
  }
}

startIndex と endIndex を取得したら、対応するデータをレンダリングできます。

レンダリング表示コンテンツ = () => {
  const { rowHeight, startIndex, endIndex } = this;
  定数コンテンツ = [];
  
  // ここで <= を使用して x+1 要素をレンダリングし、スクロールを連続的にすることに注意してください (常に判断してレンダリングし、x+2 をレンダリングします)
  (i = 開始インデックス; i <= 終了インデックス; ++i とします) {
    // rowRenderer は、インデックス i と現在の位置に対応するスタイルを受け取る必要がある、ユーザー定義のリスト要素レンダリング メソッドです。
    コンテンツ.push(
      行レンダラー({
        インデックス: i, 
        スタイル: {
          幅: '100%'、
          高さ: rowHeight + 'px'、
          位置: "絶対"、
          左: 0,
          右: 0,
          上: i * 行の高さ、
          borderBottom: "1px 実線 #000",
        }
      })
    );
  }
  
  コンテンツを返します。
};

オンラインデモ: codesandbox.io/s/a-naive-v…

原理:

では、このスクロール効果はどのようにして実現されるのでしょうか?まず、ユーザーがスクロールできるように、vListContainer で実際のリストの高さの「ファントム」コンテナーをレンダリングします。次に、onScroll イベントをリッスンし、ユーザーがスクロールをトリガーするたびに、現在のスクロール オフセット (上にスクロールした後にどれだけ隠れるか) に対応する開始インデックスを動的に計算します。新しい下付き文字が現在表示されている下付き文字と異なることがわかったら、値を割り当て、setState によって再描画がトリガーされます。ユーザーの現在のスクロール オフセットによってインデックスの更新がトリガーされない場合は、ファントム自体の長さにより、仮想リストは通常​​のリストと同じスクロール機能を持​​ちます。再描画がトリガーされると、startIndex が計算されるため、ユーザーはページの再描画を認識できません (現在のスクロールの次のフレームが再描画されたコンテンツと一致するため)。

最適化:

上記で実装した仮想リストの場合、クイックスライドを実行すると、リストがちらついたり、時間内にレンダリングされなかったり、空白になったりすることが容易にわかります。最初に言ったことを覚えていますか。レンダリング ユーザーに表示される行の最大数 + 「BufferSize」です。レンダリングする実際のコンテンツには、バッファの概念を追加できます (つまり、すばやくスライドするときにレンダリングする時間が足りないという問題を解決するために、上下にさらに多くの要素をレンダリングします)。最適化された onScroll 関数は次のとおりです。

onScroll(イベント: 任意) {
  ........
  // 現在の開始インデックスを計算する
  const currentStartIndex = Math.floor(scrollTop / rowHeight);
    
  // currentStartIndex が startIndex と異なる場合 (データを更新する必要があります)
  (現在の開始インデックス !== origin開始インデックス) の場合 {
    // startIndex と同じ役割を果たす originStartIdx という新しい変数を導入したことに注意してください。
    // 同じ効果で、現在の実際の開始インデックスを記録します。
    this.originStartIdx = 現在の開始インデックス;
    // startIndex のヘッダー バッファー計算を実行します。this.startIndex = Math.max(this.originStartIdx - bufferSize, 0);
    // endIndexで末尾バッファの計算を実行します this.endIndex = Math.min(
      this.originStartIdx + this.limit + bufferSize、
      合計 - 1
    );

    scrollTop 要素を scrollTop 要素に設定します。
  }
}

オンラインデモ: codesandbox.io/s/A-better-…

0x2 リスト要素の高さの調整

「固定高さ」の要素の仮想リストを実装したので、リストの高さが固定されておらず、非常に長いビジネス シナリオに遭遇した場合はどうなるでしょうか。

  • 一般に、高さが不定のリスト要素に遭遇したときに仮想リストを実装する方法は 3 つあります。

1. 入力データを変更し、各要素に対応する高さを渡します。dynamicHeight[i] = xxは要素iの行の高さです。

各要素の高さを知る必要がある(非現実的)

2. 現在の要素をまず画面外に描画し、測定用の高さを揃えてから、ユーザーの表示領域にレンダリングします。

この方法はレンダリングコストを2倍にするのと同じである(実用的ではない)

3. 最初に行の高さを推定してレンダリングするためにestimatedHeightプロパティを渡します。レンダリングが完了したら実際の行の高さを取得し、更新してキャッシュします。

追加の変換が導入されます (許容可能)。追加の変換が必要な理由については後で説明します...

  • ちょっと HTML の部分に戻りましょう。
<!--バージョン 1.0 -->
<div className="vListContainer">
  <div className="ファントムコンテンツ">
    ...
    <!-- 項目-1 -->
    <!-- 項目 2 -->
    <!-- 項目-3 -->
    ....
  </div>
</div>


<!--バージョン 1.1 -->
<div className="vListContainer">
  <div className="ファントムコンテンツ" />
  <div className="実際のコンテンツ">
    ...
    <!-- 項目-1 -->
    <!-- 項目 2 -->
    <!-- 項目-3 -->
    ....
  </div>
</div>
  • 「固定高さ」の仮想リストを実装したとき、phantomContent コンテナ内の要素をレンダリングし、各項目の位置を絶対値に設定し、top 属性を i * rowHeight に定義して、スクロール方法に関係なく、レンダリングされたコンテンツが常にユーザーの表示範囲内にあるようにしました。リストの高さが不明な場合、estimatedHeight を使用して現在の要素の y 位置を正確に計算することはできません。そのため、この絶対的な配置を行うのに役立つコンテナーが必要になります。
  • 実際コンテンツは、新しく導入されたリスト コンテンツ レンダリング コンテナーです。このコンテナーに position: absolute プロパティを設定することで、各項目に設定する必要がなくなります。
  • 違いは 1 つあり、代わりにactualContent コンテナを使用する点です。スライドする場合、コンテナが常にユーザーのビューポート内に表示されるように、コンテナの位置に対して y 変換を動的に実行する必要があります。
getTransform() {
  定数scrollTopをthis.stateに設定します。
  const { rowHeight、 bufferSize、 originStartIdx } = this;

  // 現在のスライドオフセット - 現在の切り捨てられた(完全に消えていない)距離 - ヘッドバッファ距離 return `translate3d(0,${
    スクロールトップ -
    (スクロールトップ % 行の高さ) -
    Math.min(originStartIdx, bufferSize) * rowHeight
  }px,0)`;

}

オンラインデモ: codesandbox.io/s/av-list-…

(注: 高度な適応性がなく、セルの再利用が実装されていない場合、ファントム内の要素を absolute 経由でレンダリングする方が transform 経由よりもパフォーマンスが向上します。これは、コンテンツがレンダリングされるたびに再配置されるためですが、transform を使用すると (再配置 + transform) > 再配置に相当します)

  • 適応型リスト要素の高さの質問に戻ります。内部で通常のブロック レイアウトを実行できる要素レンダリング コンテナー (actualContent) ができたので、高さを指定せずにすべてのコンテンツを直接レンダリングできるようになりました。これまで高さの計算に rowHeight を使用する必要があった箇所については、計算に EstimationHeight を統一的に使用して置き換えました。

制限 = Math.ceil(高さ / 推定高さ)
ファントム高さ = 合計 * 推定高さ

  • 同時に、レンダリング後に各要素の高さを繰り返し計算することを避けるために(getBoundingClientReact().height)、これらの高さを格納する配列が必要です。
インターフェースCachedPosition {
  index: number; // 現在の要素に対応する下付き文字 postop: number; // 上位置bottom: number; // 下位置height: number; // 要素の高さdValue: number; // 高さは前と異なるか (推定値)}

キャッシュされた位置: CachedPosition[] = [];

// cachedPositions を初期化する
initCachedPositions = () => {
  const {estimatedRowHeight} = this;
  this.cachedPositions = [];
  (i = 0 とします; i < this.total; ++i) {
    this.cachedPositions[i] = {
      インデックス: i,
      height:estimatedRowHeight, // 最初にestimatedHeightを使用して高さを推定します top: i *estimatedRowHeight, // 上記と同じ bottom: (i + 1) *estimatedRowHeight, // 上記と同じ
      d値: 0,
    };
  }
};
  • cachedPositions を計算 (初期化) した後、各要素の上部と下部を計算するため、phantom の高さは cachedPositions の最後の要素の下部の値になります。
this.phantomHeight = this.cachedPositions[cachedPositionsLen - 1].bottom;
  • EstimateHeight に従ってユーザーのビューポートに要素をレンダリングした後、レンダリングされた要素の実際の高さを更新する必要があります。この時点で、componentDidUpdate ライフサイクル フックを使用して計算、判断、更新を行うことができます。
コンポーネントを更新しました() {
  ......
  //actualContentRef が存在する必要があります。現在 (すでにレンダリング済み) + 合計は 0 より大きくなければなりません。
  this.actualContentRef.current と this.total > 0 の場合 {
    キャッシュされた位置を更新します。
  }
}

キャッシュされた位置を更新 = () => {
  // キャッシュされたアイテムの高さを更新
  定数ノード: NodeListOf<any> = this.actualContentRef.current.childNodes;
  定数開始 = ノード[0];

  // 表示されている各ノードの高さの差を計算します...
  ノード.forEach((ノード: HTMLDivElement) => {
    if (!ノード) {
      // スクロールが速すぎますか?...
      戻る;
    }
    定数 rect = node.getBoundingClientRect();
    const { 高さ } = rect;
    定数インデックス = Number(node.id.split('-')[1]);
    const oldHeight = this.cachedPositions[index].height;
    const dValue = oldHeight - 高さ;

    if (dValue) {
      this.cachedPositions[index].bottom -= dValue;
      this.cachedPositions[インデックス].height = 高さ;
      this.cachedPositions[インデックス].dValue = dValue;
    }
  });

  // 高さを 1 回更新します...
  startIdx = 0 とします。
  
  if (開始) {
    startIdx = Number(start.id.split('-')[1]);
  }
  
  const cachedPositionsLen = this.cachedPositions.length;
  累積DiffHeightをthis.cachedPositions[startIdx].dValueとします。
  this.cachedPositions[startIdx].dValue = 0;

  (i = startIdx + 1; i < cachedPositionsLen; ++i) の場合 {
    定数項目 = this.cachedPositions[i];
    // 高さを更新
    this.cachedPositions[i].top = this.cachedPositions[i - 1].bottom;
    this.cachedPositions[i].bottom = this.cachedPositions[i].bottom - 累積DiffHeight;

    (item.dValue !== 0)の場合{
      累積DiffHeight += item.dValue;
      項目.d値 = 0;
    }
  }

  // ファントムdivの高さを更新します
  定数height = this.cachedPositions[cachedPositionsLen - 1].bottom;
  this.phantomHeight = 高さ;
  this.phantomContentRef.current.style.height = `${height}px`;
};
  • すべての要素の正確な高さと位置の値がわかったので、現在の scrollTop (Offset) に対応する開始要素を取得するメソッドを変更して、cachedPositions を通じて取得するようにします。

cachedPositions は順序付けられた配列なので、バイナリ検索を使用して検索時の時間の複雑さを軽減できます。

getStartIndex = (スクロールトップ = 0) => {
  idx = binarySearch<CachedPosition, number>(this.cachedPositions, scrollTop, 
    (現在の値: CachedPosition、ターゲット値: 数値) => {
      現在の比較値 = 現在の値.bottom;
      (現在の比較値 === ターゲット値) の場合 {
        CompareResult.eq を返します。
      }

      現在の比較値 < ターゲット値) {
        CompareResult.lt を返します。
      }

      CompareResult.gt を返します。
    }
  );

  定数targetItem = this.cachedPositions[idx];

  // バイナリ検索の場合、非表示データ (現在表示されているデータの idx - 1) が提供されます...
  ターゲット項目が下端でスクロールトップの場合
    idx += 1;
  }

  idx を返します。
};

  

onScroll = (イベント: 任意) => {
  evt.target === this.scrollingContainer.current の場合 {
    ....
    定数 currentStartIndex = this.getStartIndex(scrollTop);
    ....
  }
};
  • バイナリ検索の実装:
エクスポート列挙型CompareResult {
  等価 = 1、
  lt、
  gt、
}



関数binarySearch<T, VT>(リスト: T[], 値: VT, compareFunc: (現在: T, 値: VT) => CompareResult)をエクスポートします。
  開始 = 0 とします。
  end = list.length - 1 とします。
  tempIndex = null とします。

  (開始 <= 終了) の間 {
    tempIndex = Math.floor((開始 + 終了) / 2);
    const midValue = リスト[tempIndex];
    const compareRes: CompareResult = compareFunc(midValue, value);

    比較結果 === 比較結果.eq) の場合 {
      tempIndex を返します。
    }
    
    比較結果 === 比較結果.lt の場合 {
      開始 = tempIndex + 1;
    } そうでない場合 (compareRes === CompareResult.gt) {
      終了 = tempIndex - 1;
    }
  }

  tempIndex を返します。
}
  • 最後に、スクロール後の変換を取得するメソッドは次のように変換されます。
getTransform = () =>
    `translate3d(0,${this.startIndex >= 1 ? this.cachedPositions[this.startIndex - 1].bottom : 0}px,0)`;

オンラインデモ: codesandbox.io/s/av-list-…

以上が、React で適応性の高い仮想リストを実装する方法の詳細です。React の適応型仮想リストの詳細については、123WORDPRESS.COM の他の関連記事にも注目してください。

以下もご興味があるかもしれません:
  • 仮想DOMの操作によるReact Viewレンダリングのシミュレーションの詳細な説明
  • Reactの最大のハイライトである仮想DOMについて簡単に説明します。
  • Reactの仮想DOMとdiffアルゴリズムの詳細な説明

<<:  Docker コンテナでネットワーク リクエストが遅くなる問題の解決策

>>:  MySqlのインストールとアンインストールに関する詳細なチュートリアル

推薦する

Linuxロスレス展開方法

概要クラウド プラットフォームのお客様のサーバーでは、業務量が拡大し続けるとディスク容量が不足する場...

初心者のためのWebページ作成: HTMLのハイパーリンクAタグの使い方を学ぶ

ハイパーリンク a タグはリンク ポイントを表し、英語の単語「anchor」の略語です。その機能は、...

CSS で画像アダプティブ コンテナを実装するためのサンプル コード

多くの場合、画像をコンテナのサイズに合わせて調整する必要があります。 1. imgタグ方式幅と高さを...

HTML チュートリアル、簡単に学べる HTML 言語 (2)

*******************HTML言語入門(パート2)*****************...

HTML における相対と絶対の使用法と違いの詳細な説明

HTML における相対と絶対の違い: 正直に言うと、HTML は世界で最もシンプルな言語です。タグ言...

layui をベースにしたログインページの実装

この記事の例では、ログインページを実装するためのlayuiの具体的なコードを参考までに共有しています...

MySQL PXC は IST 送信のみで新しいノードを構築します (推奨)

需要シナリオ: 既存の PXC 環境には大量のデータがあります。新しく購入したサーバーをこのクラスタ...

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

Reactのdiffアルゴリズムの理解diffアルゴリズムは、 Virtual DOMの変更された部...

Vueはツリーテーブルを実装する

この記事では、ツリーテーブルを実装するためのVueの具体的なコードを例として紹介します。具体的な内容...

MySQL 半同期レプリケーションの原理構成と導入の詳細な説明

環境の紹介: Ubuntu Server 16.04.2+MySQL 5.7.17 コミュニティ サ...

テーブル設定の背景画像が100%表示されない解決策

開発中に以下の状況が発見されました。 (1) ファイルが.jspファイル拡張子で保存されている場合、...

JavaScript カスタム カレンダー効果

この記事では、JavaScriptカスタムカレンダーエフェクトの具体的なコードを参考までに紹介します...

MySQL でのバイナリ型操作

この記事は主にMySQLデータベースのバイナリ型操作を紹介し、具体的な内容を通して紹介します。MyS...

Kubernetes コントローラーとラベルの簡単な分析

目次01 k8sの一般的なコントローラーRCコントローラーデプロイメント コントローラーステートフル...