React仮想リストの実装

React仮想リストの実装

1. 背景

開発プロセスでは、常に多くのリストが表示されることになります。この規模のリストがブラウザにレンダリングされると、最終的にはブラウザのパフォーマンスが低下します。データ量が多すぎると、まずレンダリングが非常に遅くなり、次にページがすぐに停止してしまいます。もちろん、それを回避する他の方法を選択することもできます。たとえば、ページングやファイルのダウンロードなどです。ここでは、仮想リストを使用してこの問題を解決する方法について説明します。

2. バーチャルリストとは何か

最も簡単な説明: リストがスクロールすると、表示領域内のレンダリング要素が変更されます。

[リストの合計の高さ]と[視覚化領域の高さ]は、[単一のデータ項目の推定高さ]によって計算されます。そして、必要に応じて[視覚化領域の高さ]内でリストをレンダリングします。

3. 関連概念の紹介

以下では、コンポーネント内の非常に重要なパラメータ情報を紹介します。まずここで理解して印象をつかんでおくと、後で使用するときにわかりやすくなります。

  • [単一データ項目の推定高さ]: リスト内の特定の項目の特定の高さ。[固定高さ] または [動的高さ] のいずれかになります。
  • [リストの合計高さ]: すべてのデータがレンダリングされると、リストの[合計高さ]
  • [可視化領域の高さ]: 仮想リスト上にぶら下がるコンテナ。リストの表示領域
  • [表示項目の推定数]: [可視化領域の高さ]では、[単一データ項目の推定高さ]に応じて、表示されるデータ項目の数
  • [開始インデックス]: [可視化領域の高さ] 表示する最初のデータのインデックス
  • [終了インデックス]: [表示領域の高さ] 表示される最後のデータ項目のインデックス
  • [各項目位置キャッシュ]: リストの高さは固定ではないため、index、top、bottom、lineHeight 属性を含む各データ項目の高さ位置が記録されます。

4. 仮想リストの実装

仮想リストは、リストがスクロールされると、[視覚化領域の高さ] 内のレンダリング要素が変更される、と簡単に理解できます。上記で紹介した関連概念に従って、これらのプロパティに基づいて次の手順に従います。

  • コンポーネントデータ[データリスト(リソース)]と[推定高さ(estimatedItemSize)]を渡します
  • [データリスト(リソース)]と[推定高さ(estimatedItemSize)](すべてのデータがレンダリングされたときの各データのプレースホルダー)に基づいて、各データの初期位置を計算します。
  • リストの合計の高さを計算する
  • [視覚領域の高さ] CSSで制御
  • [可視化領域の高さ]に応じて、可視化領域に表示される項目の推定数を計算します。
  • 表示ウィンドウの[ヘッダーマウント要素]と[テーラーマウント要素]を初期化します。スクロールが発生すると、スクロール差とスクロール方向に応じて[ヘッダーマウント要素]と[テーラーマウント要素]を再計算します。

上記の導入手順に従って、仮想リストの実装を開始しましょう。

4.1 ドライバー開発: パラメータ分析

パラメータ例示するタイプデフォルト値
リソースソースデータ配列配列[]
推定アイテムサイズ各データの推定高さ番号32ピクセル
余分なItemRenderをカスタマイズし、他のパラメータを渡すために使用しますどれでもなし
アイテムレンダリング各データをレンダリングするためのコンポーネントリアクト.FC const ItemRender = ({ data }: Data) => (<React.Fragment>{String(data) }</React.Fragment>)
トラバーサル中にアイテムの一意のキーを生成します。リソース データ内の特定の一意の値を持つフィールドである必要があります。パフォーマンスを向上させるために使用されます。デフォルトの順序のカスタマイズ -> ID -> キー -> インデックス

4.1.1 アイテムレンダリング

React をインポートし、{useState} を 'react' から取得します。
'biz-web-library' から {VirtualList} をインポートします。
// 各データを表示するためのコンポーネントを定義します。const ItemRender = ({ data }) => {
  dindex = parseInt(data); とします。
  lineHeight = dindex % 2 ? '40px' : '80px' とします。
  戻る (
    <div style={{ lineHeight, background: dindex % 2 ? '#f5f5f5' : '#fff' }}>
      <h3>#{dindex} タイトル名</h3>
      <p>ページの高さに制限はなく、好きなように書けます</p>
    </div>
  );
};
ItemRenderMemo を React.memo に代入します。

4.1.2 データリストの初期化

// リストデータを初期化する const getDatas = () => {
  定数データ = [];
  (i = 0; i < 100000; i++ とします) {
    datas.push(`${i} アイテム`);
  }
  データを返します。
};

4.1.3 使い方

// 仮想リストエクスポートデフォルト()を使用する => {
  [リソース、setResources] = useState([]);
  const changeResources = () => {
    リソースを設定します(getDatas());
  };

  戻る (
    <div>
      <button onClick={changeResources}>クリックしてください</button>

      <div
        スタイル={{
          高さ: '400px'、
          オーバーフロー: 'auto'、
          境界線: '1px 実線 #f5f5f5'、
          パディング: '0 10px',
        }}
      >
        <仮想リスト
          アイテムレンダラー={アイテムレンダラーメモ}
          リソース={リソース}
          推定アイテムサイズ={60}
        />
      </div>
    </div>
  );
};

4.2 コンポーネントの初期化計算とレイアウト

使い方がわかったので、コンポーネントの実装を始めましょう。渡されたデータ ソース リソースと推定高さ EstimationItemSize に従って、各データの初期化位置を計算します。

// 循環キャッシュリストの全体的な初期化の高さ export const initPositinoCache = (
  推定アイテムサイズ: 数値 = 32、
  長さ: 数値 = 0、
) => {
  インデックスを0とします。
  位置 = 配列(長さ);
  while (インデックス < 長さ) {
    位置[インデックス] = {
      索引、
      高さ: 推定アイテムサイズ、
      上: インデックス * 推定アイテムサイズ、
      下部: (インデックス++ + 1) * 推定アイテムサイズ、
    };
  }
  ポジションを返す。
};

リスト内の各データの高さが一貫している場合、この高さは変更されません。各データの高さが固定されていない場合は、スクロール処理中に位置が更新されます。初期化する必要があるその他のパラメータは次のとおりです。

パラメータ例示するタイプデフォルト値
リソースソースデータ配列配列[]
開始オフセット表示領域の上部からのオフセット番号0
リストの高さすべてのデータがレンダリングされると、コンテナの高さはどれでもなし
表示数1ページあたりの視覚化領域の数番号10
開始インデックス可視化領域開始インデックス番号0
終了インデックス可視化エリア終了インデックス番号10
表示データ視覚化領域に表示されるデータ配列[]

実際、それぞれの属性については、簡単な紹介をすればその重要性がはっきりとわかります。ただし、[startOffset]パラメータを詳しく導入する必要があります。スクロール処理中に無限スクロールをシミュレートする重要なプロパティです。その値は、スクロール処理中の上からの位置を示します。 [startOffset]は[visibleData]を組み合わせることで無限スクロールの効果を実現します。
ヒント: ここでの [positions] の位置に注意してください。これはコンポーネントの外部変数に相当します。コンポーネントの静的プロパティに掛けないように注意してください。

// すべての項目の位置をキャッシュします。let positions: Array<PositionType>;

クラスVirtualListはReact.PureComponentを拡張します{
 
  コンストラクタ(props) {
    スーパー(小道具);
    const {リソース} = this.props;

    // キャッシュを初期化する positions = initPositinoCache(props.estimatedItemSize, resources.length);
    この状態 = {
      リソース、
      開始オフセット: 0,
      listHeight: getListHeight(positions), // 位置の最後のデータの下部属性 scrollRef: React.createRef(), // 仮想リストコンテナ参照
      items: React.createRef(), // 仮想リスト表示領域参照
      visibleCount: 10, // ページ上の表示領域の数 startIndex: 0, // 表示領域の開始インデックス endIndex: 10, // // 表示領域の終了インデックス };
  }
  // TODO: 他の機能を非表示にします。 。 。 。 。


  //レイアウトレンダリング() {
  const { ItemRender = ItemRenderComponent、 extension } = this.props;
  const { listHeight、startOffset、resources、startIndex、endIndex、items、scrollRef } = this.state;
  visibleData を resources.slice(startIndex, endIndex); とします。

  戻る (
    <div ref={scrollRef} スタイル={{ 高さ: `${listHeight}px` }}>
      <ul
        ref={アイテム}
        スタイル={{
          変換: `translate3d(0,${startOffset}px,0)`,
        }}
      >
        {visibleData.map((データ, インデックス) => {
          戻る (
            <li key={data.id || data.key || index} data-index={`${startIndex + index}`}>
              <ItemRender data={data} {...extrea}/>
            </li>
          );
        })}
      </ul>
    </div>
  );
  }
} 

4.3 スクロールすると登録イベントと更新がトリガーされます

[componentDidMount] を通じて onScroll を DOM に登録します。スクロール イベントでは、requestAnimationFrame が使用されます。このメソッドは、ブラウザのアイドル時間を利用して実行されるため、コードのパフォーマンスが向上します。より深く理解したい場合は、この API の具体的な使用方法を確認してください。

コンポーネントマウント() {
  イベントをオン(this.getEl(), 'スクロール', this.onScroll, false);
  events.on(this.getEl(), 'マウスホイール', NOOP, false);

  // レンダリングに基づいて最新のノードを計算します。let visibleCount = Math.ceil(this.getEl().offsetHeight / EstimationItemSize);
  可視カウント === this.state.visibleCount || 可視カウント === 0 の場合 {
    戻る;
  }
  // visibleCount が変更されたため、endIndex、listHeight/offset を更新します。this.updateState({ visibleCount, startIndex: this.state.startIndex });
}

getEl = () => {
    el = this.state.scrollRef || this.state.items とします。
    parentEl: any = el.current?.parentElement; とします。
    スイッチ(window.getComputedStyle(parentEl)?.overflowY) {
      ケース 'auto':
      ケース 'スクロール':
      ケース 'オーバーレイ':
      '可視'の場合:
        parentEl を返します。
    }
    document.body を返します。
};

スクロールのオン = () => {
    リクエストアニメーションフレーム(() => {
      scrollTop を this.getEl() とします。
      startIndex = binarySearch(positions, scrollTop); とします。

      // startIndex が変更されたため、endIndex、listHeight/offset を更新します。this.updateState({ visibleCount: this.state.visibleCount, startIndex});
    });
  };

次に、重要なステップを分析します。スクロールすると、現在の [scrollRef] 仮想リスト コンテナーの [scrollTop] を取得できます。この距離と [positions] (各項目のすべての位置プロパティを記録) を通じて、その位置の startIndex を取得できます。パフォーマンスを向上させるために、バイナリ検索を使用します。

// ツール関数、ツールファイルに格納 export const binarySearch = (list: Array<PositionType>, value: number = 0) => {
  開始します: 番号 = 0;
  終了: number = list.length - 1;
  tempIndex = null とします。
  (開始 <= 終了) の間 {
    midIndex = Math.floor((start + end) / 2);とします。
    midValue = list[midIndex].bottom;とします。

    // 値が等しい場合は、見つかったノードが直接返されます(一番下なので、startIndexは次のノードである必要があります)
    if (midValue === 値) {
      midIndex + 1 を返します。
    }
    // 中間値が入力値より小さい場合、値に対応するノードが開始値より大きいことを意味し、開始値は 1 つ前に戻ります。else if (midValue < value) {
      開始 = midIndex + 1;
    }
    // 中間値が入力値より大きい場合、その値は中間値より前であることを意味し、終了ノードは中間 - 1 に移動します。
    そうでない場合(中間値>値){
      // tempIndexは値に最も近いすべての値を格納します if (tempIndex === null || tempIndex > midIndex) {
        tempIndex = 中間インデックス;
      }
      終了 = midIndex - 1;
    }
  }
  tempIndex を返します。
};

startIndex を取得したら、startIndex に基づいてコンポーネント State 内のすべてのプロパティの値を更新します。

 更新状態 = ({ 可視カウント、開始インデックス }) => {
    // 新しく計算されたノードに従ってデータを更新する this.setState({
      開始オフセット: 開始インデックス >= 1 ? 位置[開始インデックス - 1]?.bottom : 0,
      リストの高さ: getListHeight(位置),
      開始インデックス、
      表示数、
      終了インデックス: getEndIndex(this.state.resources, 開始インデックス, 可視カウント)
    });
  };

// 以下は他のファイルに配置されるツール関数です export const getListHeight = (positions: Array<PositionType>) => {
    index = positions.length - 1 とします。
    インデックス < 0 ? 0 を返す: positions[index].bottom;
  };

エクスポートconst getEndIndex = (
  リソース: 配列<データ>、
  開始インデックス: 数値、
  表示数: 数値、
) => {
  resourcesLength を resources.length とします。
  endIndex = startIndex + visibleCount とします。
  resourcesLength > 0 を返します。Math.min(resourcesLength, endIndex) : endIndex;
}

4.4 アイテムの高さが等しくない場合は更新する

この時点で、基本的な DOM スクロール、データ更新、その他のロジックは完了しました。しかし、テスト中に、高さが等しくない場合、位置やその他の操作が更新されていないことがわかりますか?これらをどこに置けばいいでしょうか?
ここで、[componentDidUpdate] が役に立ちます。 DOM がレンダリングされるたびに、表示される項目の位置と高さの情報が [position] 属性に更新される必要があります。現在の合計高さ [istHeight] とオフセット [startOffset] も同時に更新する必要があります。

 コンポーネントを更新しました() {
  this.updateHeight();
}

  
高さの更新 = () => {
  items: HTMLCollection = this.state.items.current?.children; とします。
  if (!items.length) が return;

  // キャッシュを更新 updateItemSize(positions, items);

  // 合計の高さを更新します let listHeight = getListHeight(positions);

  // 合計オフセットを更新します。let startOffset = getStartOffset(this.state.startIndex, positions);

  this.setState({
    リストの高さ、
    開始オフセット、
  });
};

// 以下は他のファイルに配置されるツール関数です export const updateItemSize = (
  位置: 配列<位置タイプ>,
  アイテム: HTMLコレクション、
) => {
  Array.from(items).forEach(item => {
    index = Number(item.getAttribute('data-index')); とします。
    高さを item.getBoundingClientRect() に設定します。
    oldHeight = positions[index].height;とします。

    // 差異がある場合は、このノード以降のすべてのノードを更新します。let dValue = oldHeight - height;
    if (dValue) {
      位置[インデックス].bottom = 位置[インデックス].bottom - dValue;
      位置[インデックス].height = 高さ;

      (k = インデックス + 1 とします; k < positions.length; k++) {
        位置[k].top = 位置[k - 1].bottom;
        位置[k].bottom = 位置[k].bottom - dValue;
      }
    }
  });
};

//現在のオフセットを取得する export const getStartOffset = (
  開始インデックス: 数値、
  位置: Array<PositionType> = [],
) => {
  startIndex >= 1 ? positions[startIndex - 1]?.bottom : 0 を返します。
};

エクスポートconst getListHeight = (positions: Array<PositionType>) => {
  index = positions.length - 1 とします。
  インデックス < 0 ? 0 を返す: positions[index].bottom;
};

4.5 外部パラメータデータの変更、コンポーネントデータの更新

この最後のステップでは、渡した外部データ ソースが変更された場合、データを同期する必要があります。この操作は、もちろん getDerivedStateFromProps メソッドで完了します。

 静的 getDerivedStateFromProps()
    次のプロパティ: 仮想リストプロパティ、
    前の状態: 仮想リスト状態、
  ){
    const { リソース、estimatedItemSize } = nextProps;
    if (リソース !== prevState.resources) {
      位置 = initPositinoCache(推定アイテムサイズ、リソースの長さ);

      // 高さを更新します let listHeight = getListHeight(positions);

      // 合計オフセットを更新します。let startOffset = getStartOffset(prevState.startIndex, positions);

     
      endIndex = getEndIndex(resources, prevState.startIndex, prevState.visibleCount); とします。
     
      戻る {
        リソース、
        リストの高さ、
        開始オフセット、
        終了インデックス、
      };
    }
    null を返します。
  }

5 結論

これで、完全な仮想リスト コンポーネントが完成しました。各データの ItemRender のレンダリング関数がカスタマイズされているため、リスト形式であれば、実質的にどの項目でもスクロールできます。もちろん、オンラインで読んだ情報によると、ネットワークの問題により、画像のスクロールではリスト項目の実際の高さを保証できず、不正確さが生じる可能性があります。これについては今のところここでは説明しません。興味のある方はさらに詳しく説明してください。

React 仮想リストの実装に関するこの記事はこれで終わりです。React 仮想リストに関するより関連性の高いコンテンツについては、123WORDPRESS.COM の過去の記事を検索するか、以下の関連記事を引き続き閲覧してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • Reactは適応性の高い仮想リストを実装する

<<:  MySQL 8.0.12 のインストールと設定方法のグラフィック チュートリアル (Windows10)

>>:  VMware 仮想化 KVM のインストールと展開のチュートリアルの概要

推薦する

Linux でのプロセスデーモン スーパーバイザーのインストール、構成、および使用

Supervisor は非常に優れたデーモン管理ツールです。自動起動、ログ出力、自動ログカットなど、...

WeChatアプレットで計算機機能を実装する

この記事は、WeChat アプレットを使用して作成された簡単な計算機です。興味のある方はご覧ください...

Linux ファイル記述子、ファイルポインタ、および inode の詳細

目次Linux - ファイル記述子、ファイルポインタ、インデックスノード1. Linux - ファイ...

Dockerはコード検出プラットフォームSonarQubeを構築し、Mavenプロジェクトのプロセスを検出します

1 はじめに優れたコーディング習慣は優れたプログラマーが備えるべき資質ですが、コードの品質を保証する...

埋め込みJavaScriptと外部リンクの基本的な応用方法

目次埋め込みJavaScriptと外部リンクの基本的な応用JavaScript の記述方法には、イン...

UbuntuにMySQLをインストールするときにデフォルトのパスワードを変更する詳細な手順

ステップ1: ディレクトリに入ります: cd /etc/mysql、debian.cnfファイルを表...

JavaScript のクロージャの詳細な説明

導入クロージャは JavaScript の非常に強力な機能です。いわゆるクロージャは関数内の関数です...

MySQL 時間差関数 (TIMESTAMPDIFF、DATEDIFF)、日付変換計算関数 (date_add、day、date_format、str_to_date)

1. 時間差関数(TIMESTAMPDIFF、DATEDIFF) MySQLを使用して時間差を計算...

シンプルなカレンダー効果を実現する js

この記事では、シンプルなカレンダー効果を実現するためのjsの具体的なコードを参考までに共有します。具...

モバイルレイアウトにvw+remを使用する方法

まだ rem フレキシブルレイアウトを使用していますか?圧縮された js コードの大きなセクションを...

Windows 64 ビットに MySQL を再インストールするチュートリアル (Zip バージョン、解凍バージョンの MySQL インストール)

MySQLをアンインストールする1. コントロールパネルで、MySQLのすべてのコンポーネントをア...

dockerを使用してdubboプロジェクトをデプロイする方法

1. まず、Springbootを使用して簡単なDubboテストプログラムを構築し、関連する依存関係...

VUE ユニアプリの条件付きコーディングとページレイアウトに関する簡単な説明

目次条件付きコンパイルページレイアウト要約する条件付きコンパイル条件付きコンパイルでは、特別なコメン...

MySQL 8.0 アップグレード体験

目次序文1. まず、既存のバージョンの MySQL を完全にアンインストールします。 2. deb ...

vue プロジェクトで rem を使用して px を置き換える例

目次道具プラグインをインストールするプロジェクトのルートディレクトリに.postcssrc.jsファ...