最近、あるプラットフォームの開発と反復中に、非常に長いリストが antd モーダルにネストされ、読み込みが遅くなり、動作が停止するという状況に遭遇しました。そこで、全体的なエクスペリエンスを最適化するために、仮想スクロール リストをゼロから実装することにしました。 変換前:
変換後:
パフォーマンス比較デモ: codesandbox.io/s/av-list-… 0x0の基本では、仮想スクロール/リストとは何でしょうか?
(上の図から、ユーザーが毎回見ることができる実際の要素/コンテンツは、item-4からitem-13までの9つの要素のみであることがわかります) 0x1 「固定高さ」の仮想リストを実装するまず、いくつかの変数/名前を定義する必要があります。
表示領域内のコンテンツのみをレンダリングするため、コンテナ全体の動作を長いリスト(スクロール)に似たものに保つには、元のリストの高さを維持する必要があり、HTML構造を次のように設計します。 <!--バージョン 1.0 --> <div className="vListContainer"> <div className="ファントムコンテンツ"> ... <!-- 項目-1 --> <!-- 項目 2 --> <!-- 項目-3 --> .... </div> </div> で:
次に、onScroll 応答関数を vListContainer にバインドし、ネイティブ スクロール イベントの scrollTop プロパティに従って関数内の startIndex と endIndex を計算します。
固定リスト要素の高さが必要です: rowHeight
リストの合計の高さ: phantomHeight = total * 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", } }) ); } コンテンツを返します。 };
原理:では、このスクロール効果はどのようにして実現されるのでしょうか?まず、ユーザーがスクロールできるように、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 要素に設定します。 } }
0x2 リスト要素の高さの調整
1. 入力データを変更し、各要素に対応する高さを渡します。dynamicHeight[i] = xxは要素iの行の高さです。
2. 現在の要素をまず画面外に描画し、測定用の高さを揃えてから、ユーザーの表示領域にレンダリングします。
3. 最初に行の高さを推定してレンダリングするためにestimatedHeightプロパティを渡します。レンダリングが完了したら実際の行の高さを取得し、更新してキャッシュします。
<!--バージョン 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>
getTransform() { 定数scrollTopをthis.stateに設定します。 const { rowHeight、 bufferSize、 originStartIdx } = this; // 現在のスライドオフセット - 現在の切り捨てられた(完全に消えていない)距離 - ヘッドバッファ距離 return `translate3d(0,${ スクロールトップ - (スクロールトップ % 行の高さ) - Math.min(originStartIdx, bufferSize) * rowHeight }px,0)`; }
(注: 高度な適応性がなく、セルの再利用が実装されていない場合、ファントム内の要素を absolute 経由でレンダリングする方が transform 経由よりもパフォーマンスが向上します。これは、コンテンツがレンダリングされるたびに再配置されるためですが、transform を使用すると (再配置 + transform) > 再配置に相当します)
制限 = Math.ceil(高さ / 推定高さ)
インターフェース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, }; } };
this.phantomHeight = this.cachedPositions[cachedPositionsLen - 1].bottom;
コンポーネントを更新しました() { ...... //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`; };
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)`;
以上が、React で適応性の高い仮想リストを実装する方法の詳細です。React の適応型仮想リストの詳細については、123WORDPRESS.COM の他の関連記事にも注目してください。 以下もご興味があるかもしれません:
|
<<: Docker コンテナでネットワーク リクエストが遅くなる問題の解決策
>>: MySqlのインストールとアンインストールに関する詳細なチュートリアル
概要クラウド プラットフォームのお客様のサーバーでは、業務量が拡大し続けるとディスク容量が不足する場...
ハイパーリンク a タグはリンク ポイントを表し、英語の単語「anchor」の略語です。その機能は、...
多くの場合、画像をコンテナのサイズに合わせて調整する必要があります。 1. imgタグ方式幅と高さを...
*******************HTML言語入門(パート2)*****************...
HTML における相対と絶対の違い: 正直に言うと、HTML は世界で最もシンプルな言語です。タグ言...
この記事の例では、ログインページを実装するためのlayuiの具体的なコードを参考までに共有しています...
需要シナリオ: 既存の PXC 環境には大量のデータがあります。新しく購入したサーバーをこのクラスタ...
Reactのdiffアルゴリズムの理解diffアルゴリズムは、 Virtual DOMの変更された部...
MySQLはユーザーを作成し、ユーザーの権限を承認および取り消します動作環境: MySQL 5.0...
この記事では、ツリーテーブルを実装するためのVueの具体的なコードを例として紹介します。具体的な内容...
環境の紹介: Ubuntu Server 16.04.2+MySQL 5.7.17 コミュニティ サ...
開発中に以下の状況が発見されました。 (1) ファイルが.jspファイル拡張子で保存されている場合、...
この記事では、JavaScriptカスタムカレンダーエフェクトの具体的なコードを参考までに紹介します...
この記事は主にMySQLデータベースのバイナリ型操作を紹介し、具体的な内容を通して紹介します。MyS...
目次01 k8sの一般的なコントローラーRCコントローラーデプロイメント コントローラーステートフル...