仮想スクロールを簡単に実装するためのVueサンプルコード

仮想スクロールを簡単に実装するためのVueサンプルコード

序文

モバイル Web ページの日常的な開発では、長いリストをレンダリングする必要があるシナリオが時々あります。たとえば、旅行 Web サイトでは、全国の都市のリストを完全に表示し、次にアドレス帳のすべての名前を A、B、C のアルファベット順に表示する必要があります...

通常、長いリストの数が数百以内であれば予期しない影響はなく、ブラウザ自体も十分にサポートできます。ただし、数が数千に達すると、ページのレンダリング プロセスが明らかに停止します。数が数万、さらには数十万を超えると、Web ページが直接クラッシュする可能性があります。

長いリストによって引き起こされるレンダリング圧力を解決するために、業界では対応する対応技術、つまり長いリストの仮想スクロールが開発されました。

仮想スクロールの本質は、ページがどのようにスライドしても、HTML ドキュメントは現在の画面ビューポートに表示される少数の DOM 要素のみをレンダリングすることです。

長いリストに 10 万件のデータがあると仮定すると、ユーザーにとって画面に表示されるのは 10 数件のデータだけです。そのため、ページがスライドするときに、スクロール イベントをリッスンしてビューポート データをすばやく切り替えることで、スクロール効果を高度にシミュレートできます。

仮想スクロールでは、同様のスクロール効果をシミュレートするために少数の DOM 要素をレンダリングするだけで済むため、フロントエンド エンジニアは数万、あるいは数十万もの項目からなる長いリストを開発できるようになります。

次の画像は、携帯電話で見ることができる世界中の都市の長いリストを示しています (ソース コードは記事の最後に掲載されています)。

ローリング原理

仮想スクロールの仕組みを理解するには、まず次の図をご覧ください。指を下にスライドすると、HTML ページも上にスクロールします。
画像にマークされた距離から、次の結論を導き出すことができます。画面ビューポートの上端が、ID が item の div 要素の上端と一致する場合、item 要素と長いリストの上部の間の距離は、ページの scrollTop 距離と正確に等しくなります (この結論は、後で距離を計算するときに使用されます)。

リアルなスクロール効果をシミュレートするには、まず仮想スクロールが次の 2 つの要件を満たす必要があります。

  • 仮想スクロールリストのスクロールバーは、通常のリストのスクロールバーと一致しています。たとえば、リストに 1,000 個のデータ項目が含まれており、ブラウザーが通常のレンダリング方法を使用する場合、スクロールバーを一番下に付けるには 5,000 ピクセル下にスクロールする必要があると想定します。その後、仮想スクロール技術を適用した後、スクロールバーも同じ特性を持ち、一番下に付けるには 5,000 ピクセル下にスクロールする必要があります。
  • 仮想スクロールでは、ビューポートと上部および下部の一部の DOM 要素のみがレンダリングされます。スクロール バーが下にスライドすると、長いリストを通常どおりレンダリングするときに表示されるコンテンツと一致するように、ビューのコンテンツをリアルタイムで更新する必要があります。

上記の要件を満たすための HTML デザイン構造は次のようになります。

.wrapper は最も外側のコンテナ要素であり、位置は絶対または相対に設定され、子要素はそれに基づいて配置されます。

サブ要素 .background と .list は、仮想スクロールの鍵となります。.background は空の div ですが、長いリスト内のすべてのリスト項目の高さの合計と同じ高さに設定する必要があります。さらに、絶対位置に設定し、z-index 値を -1 に設定する必要があります。

.list は、ビューポートによって観察される Dom 要素を動的にレンダリングする役割を担い、位置は絶対位置に設定されます。

<テンプレート>
  <div class="wrapper">
    <div class="background" :style="{height:`${total_height}px`}"></div>
    <div class="list">
      <div class="line">
        <div class="item lt">北京</div>
        <div class="item gt">北京</div>
      </div>
      <div class="line">
        <div class="item lt">上海</div>
        <div class="item gt">上海</div>
      </div>
      <div class="line">
        <div class="item lt">広州</div>
        <div class="item gt">広州</div>
      </div>
      ... //省略</div>
  </div>
</テンプレート>
<style lang="scss" スコープ>
.ラッパー{
  位置: 絶対;
  左: 0;
  右: 0;
  下部: 0;
  上: 60px;
  overflow-y: スクロール;
  。背景 {
    位置: 絶対;
    上: 0;
    左: 0;
    右: 0;
    Zインデックス: -1;
  }
  .リスト{
    位置: 絶対;
    上: 0;
    左: 0;
    右: 0;
  }
}
</スタイル>

上記コードで total_height が 10000px に等しい場合、ページ実行効果図は次のようになります。
子要素 .background の高さが設定されているため、親要素 .wrapper が子要素によってサポートされ、スクロールバーが表示されます。
この時下にスライドすると、2つの子要素 ​​.background と .list が同時に上にスクロールします。スクロール距離が 9324px に達すると、スクロールバーも一番下まで到達します。
これは、親要素 .wrapper 自体の高さが 676 ピクセルで、スライド距離の 9324 ピクセルを加えると、結果がリストの合計の高さ 10000 ピクセルと正確に等しくなるためです。
上記の動作を観察すると、.background は単なる空の div ですが、リスト全体の高さを指定することで、右側のスクロール バーの外観と動作を、通常の長いリストのレンダリングによって生成されるスクロール バーと一貫性のあるものにできることがわかります。

スクロール バーの問題は解決しましたが、スクロール バーが下にスライドすると、データ リストが上に移動します。リストが完全に画面から移動した後、次のスライドはすべて白い画面になります。
白い画面の問題を解決するには、ビューポートにスライドデータを常に表示する必要があります。次に、.list 要素は、スライド距離に応じて自身の絶対位置の top 値を動的に更新し、.list が画面外に描画されないようにします。同時に、現在のビューポートに表示する必要があるデータは、スライド距離に応じて動的にレンダリングされる必要があります。

次のアニメーションを観察してください。右側の Dom 構造はスライドするときの変化を示しています。

スクロール バーがすばやく下にスライドすると、リストの DOM 要素がすばやくレンダリングされ、更新されます。このとき、.list 内の DOM 要素が絶えず置き換えられるだけでなく、.list 要素自体も transform: translate3d(0, ? px ,0) スタイル値を絶えず変更しています (translate3d を変更すると、top 属性値を変更するのと同様の効果が得られます)。

以上の説明から、仮想スクロールの実装ロジックは明らかです。まず、js はスクロールバーのスライドイベントを監視し、スライド距離に応じて .list 要素のどのサブ要素をレンダリングするかを計算し、.list 要素の位置を更新します。スクロールバーがスライドし続けると、サブ要素と位置も常に更新され、ビューポート上でスクロール効果がシミュレートされます。

成し遂げる

開発されたデモ ページを下図に示します。リスト項目には次の 3 つの構造が含まれています。

  • 小さなリスト項目。都市名の最初の文字は別の行にあり、高さは 50 ピクセルです。
  • 通常のリスト項目、左側に英語名、右側に中国語名、高さ 100 ピクセル。
  • 左側に英語名、中央に中国語名、右側に画像があり、高さが 150 ピクセルの大きなリスト項目。

リスト データ city_data の JSON 構造は次のようになります。type 1 は小さなリスト項目のスタイル構造レンダリングを表し、2 は通常のリスト項目を表し、3 は大きなリスト項目を表します。

[{"name":"A","value":"","type":1},{"name":"Al l'Ayn","value":"Ayn","type":2},{"name":"Aana","value":"Aana","type":3} ... ]

city_data には長いリスト内のすべてのデータが含まれています。city_data を取得したら、まず各項目のデータ構造を走査して調整します (コードは次のとおりです)。

次のように処理すると、各リスト項目には最終的に top 値と height 値が含まれます。top 値は長いリストの先頭からの項目の長さを示し、height 値は項目の高さを示します。
total_height はリスト全体の高さの合計で、最終的には前述の .background 要素に割り当てられます。処理されたデータは this.list に割り当てられて保存され、最小リスト項​​目の高さ this.min_height が記録されます。

  マウントされた(){
     function getHeight (type) { // タイプ値に応じて高さを返す switch (type) {
          ケース1: 50を返します。
          ケース2: 100を返します。
          ケース3: 150を返します。
          デフォルト:
            戻る "";
        }
      }
      total_height を 0 にします。
      const list = city_data.map((データ, インデックス) => {
        定数height = getHeight(data.type);
        定数ob = {
          索引、
          身長、
          上: 合計高さ、
          データ
        }
        total_height += 高さ;
        ob を返します。
      })
      this.total_height = total_height; // リストの合計の高さ this.list = list;
      this.min_height = 50; // 最小の高さは50です
      // 画面に表示できるリスト項目の最大数。containerHeight は親コンテナーの高さで、最小の高さに基づいて計算されます。this.maxNum = Math.ceil(containerHeight / this.min_height);
  }

HTML は、type 値に応じて異なるスタイル構造をレンダリングします (コードは次のようになります)。親コンテナー .wrapper はスライド イベント onScroll をバインドし、リスト要素 .list は this.list 配列をトラバースしません。これは、this.list がすべてのリスト項目を含む元のデータであるためです。

<template> テンプレートは、ビューポートに表示する必要があるデータ runList を走査するだけで済みます。配列 runList に含まれるデータは、スクロール イベントによって継続的に更新されます。

<テンプレート>
  <div class="wrapper" ref="wrapper" @scroll="onScroll">
    <div class="background" :style="{height:`${total_height}px`}"></div>
    <div class="list" ref="コンテナ">
      <div v-for="runList 内のアイテム" :class="['line',getClass(item.data.type)]" :key="item">
        <div class="item lt">{{item.data.name}}</div>
        <div class="item gt">{{item.data.value}}</div>
        <div v-if="item.data.type == 3" クラス="img-container">
          <img src="../../assets/default.png" />
        </div>
      </div>
    </div>
  </div>
</テンプレート>

スクロール イベントは onScroll メソッドをトリガーします (コードは以下のとおり)。スクロール バーは非常に頻繁にトリガーされるため、ブラウザーによる計算量を減らすために、requestAnimationFrame 関数を使用して調整します。

スクロール イベント オブジェクト e は、現在のスクロール バーのスライド距離を取得できます。距離に基づいて、runList のリスト データを計算し、.list の位置情報を変更するだけです。

 スクロールの(e){
      if (this.ticking) {
        戻る;
      }
      this.ticking = true;
      リクエストアニメーションフレーム(() => {
        this.ticking = false;
      })
      定数距離 = e.target.scrollTop;
      this.distance = 距離;
      this.getRunData(距離);
 }

スクロール距離に基づいて、画面ビューポートの下にレンダリングする必要がある最初のリスト項目要素をすばやく見つけるにはどうすればよいでしょうか?

this.list は長いリストのデータ ソースであり、各リスト項目には長いリストの先頭からの距離 top と、その項目自体の高さ height が格納されます。
上記の結論は、ページのスクロール中に、ビューポートの上端がリスト項目の上端と一致する場合、スライド距離 scrollTop はリスト項目から長いリストの上部までの距離 top と正確に等しくなるということです。

ページが少し上に移動すると、ビューポートの下の最初のリスト項目の一部だけが表示され、他の部分は画面外になります。この時点では、ビューポートの下の開始要素が画面から完全に外れるまで上に移動しない限り、ビューポートの下の開始要素は依然としてリスト項目であると判断されます。

次に、ビューポートにレンダリングされる最初の要素を判断する基準は、ページの scrollTop がリスト項目要素の上部と上部 + 高さの間にあることです。

上記の原則に基づいて、バイナリ検索を使用して高速クエリを実現できます (コードは次のとおりです)。

// バイナリ検索を使用して開始インデックスを計算します。scrollTop はスクロール距離です。getStartIndex (scrollTop) {
      開始 = 0、終了 = this.list.length - 1 とします。
      (開始 < 終了) の間 {
        const mid = Math.floor((開始 + 終了) / 2);
        const { top, height } = this.list[mid];
        スクロールトップ >= 上 && スクロールトップ < 上 + 高さ) {
          開始 = 途中;
          壊す;
        } そうでない場合 (scrollTop >= top + height) {
          開始 = 中間 + 1;
        } そうでない場合 (スクロールトップ < トップ) {
          終了 = 中間 - 1;
        }
      }
      開始を返します。
}

バイナリ メソッドは、this.list 配列のビューポートの下にレンダリングされた最初の要素のインデックスを計算します。このインデックスは開始インデックス start_index と呼ばれます。次に、コア関数 getRunData を入力します (コードは次のとおりです)。主に次の 2 つの処理を行います。

  • runList リストデータを動的に更新する
  • .list の長いリスト要素の位置を動的に更新する

実際の開発では、画面の高さが 1000px、最小のリスト項目が 50px であると仮定すると、画面に収容できるリスト項目の最大数 this.maxNum は 20 になります。
スライド距離に基づいて開始インデックス start_index を計算し、start_index に基づいてデータ ソース this.list から 20 個の要素を取得し、それらを this.runList に割り当てます。これでデータの更新は完了するのではないでしょうか。

this.runList が 1 つの画面に収まる最大数の項目のみを保持している場合、スクロール バーが高速にスクロールすると、インターフェイスのレンダリング速度が指のスライド速度に追いつかず、下部に白い画面が点滅します。

この問題の解決策は、HTML ドキュメントにバッファリングされたデータをもう少しレンダリングすることです。たとえば、以下の getRunData 関数は、それぞれ上部の画面、中央の画面、下部の画面に対応する 3 つの画面の高さに対応できるリスト項目の数をレンダリングします。

中央の画面は現在のビューポートに対応する画面で、上下の画面にはビューポートの上下に表示されていないバッファリングされた Dom が格納されます。まず、バイナリ検索方式を使用して、画面ビューポートの下の最初のリスト項目要素のインデックス start_index を照会し、次に start_index に基づいて上下の画面の最初のリスト項目のインデックスも簡単に取得できます。

 getRunData(距離 = null) {

      //スクロール距離 const scrollTop = distance ? distance : this.$refs.container.scrollTop;

      //スクロールが実行されない範囲 if (this.scroll_scale) {
        (スクロールトップ>this.scroll_scale[0]&&スクロールトップ<this.scroll_scale[1]){
          戻る;
        }
      }

      //開始インデックス let start_index = this.getStartIndex(scrollTop);
      start_index = start_index < 0 ? 0 : start_index;

      // 上部画面インデックス、this.cache_screens のデフォルトは 1 で、1 つの画面をキャッシュします。let upper_start_index = start_index - this.maxNum * this.cache_screens;
      上位開始インデックス = 上位開始インデックス < 0 ? 0 : 上位開始インデックス;

      // オフセットを調整する
      this.$refs.container.style.transform = `translate3d(0,${this.list[upper_start_index].top}px,0)`;

      //中央の画面の要素 const mid_list = this.list.slice(start_index, start_index + this.maxNum);

      // 上部画面 const upper_list = this.list.slice(upper_start_index, start_index);

      // 下画面要素 let down_start_index = start_index + this.maxNum;

      down_start_index = down_start_index > this.list.length - 1 ? this.list.length : down_start_index;

      this.scroll_scale = [this.list[Math.floor(upper_start_index + this.maxNum / 2)].top、this.list[Math.ceil(start_index + this.maxNum / 2)].top];

      定数 down_list = this.list.slice(down_start_index, down_start_index + this.maxNum * this.cache_screens);

      this.runList = [...上位リスト、...中間リスト、...下位リスト];

   }

スクロール イベントは非常に頻繁にトリガーされます。開発者としては、ブラウザの計算量をできるだけ減らす必要があります。そのため、スクロール範囲をコンポーネントにキャッシュできます。つまり、配列 this.scroll_scale (データ構造は [5000,5675] に似ています)。スライド距離がこの範囲内であれば、ブラウザはリスト データを更新する必要はありません。

スクロール距離 scrollTop がスクロール範囲内に入ると、getRunData 関数は何も行いません。指をスライドすると、デフォルトのスクロール動作を使用して、.list 要素が指で上下に移動します。

スクロール方向が下方向であると仮定すると、scrollTop がスクロール範囲外になったとき、スライディング viewport.wrapper の上端が次のリスト項目の上端と一致した瞬間に、getRunData 関数はまず開始インデックス start_index を計算し、次に start_index を通じて上画面の最初の要素のインデックス upper_start_index を取得します。

コンポーネントが以前にマウントされたときに、各リスト項目は長いリストの先頭からの距離をキャッシュしていたため、.list 要素を割り当てる位置情報は this.list[upper_start_index].top を通じて取得できます。次に、新しいリスト データ runList が再計算されてページがレンダリングされ、新しい状態でのスクロール範囲がキャッシュされます。

これまで、上記の手順により仮想スクロールが実現されました。上記で紹介した実用的な方法は非常に簡単に使用できますが、設計者は設計案を計画する際に、まずさまざまなスタイルのリスト項目の高さを定義する必要があります。

リスト項目の高さを内部のコンテンツに応じて自然に拡張する必要があり、ページ設計時に固定できない場合は、次の参考記事を読んで実現できます。
適応型リスト項目の高さによる仮想スクロールは魅力的に聞こえますが、追加の処理手順が必要になり、新しい問題に直面します (たとえば、リスト項目に非同期で読み込まれた画像が含まれている場合、高さの計算が困難になります)。また、ブラウザーの計算量も大幅に増加します。したがって、デザイン内のリスト項目で高さを定義する必要があるかどうかは、特定のシナリオによって異なります。

ソースコード

ソースコード

参照する

10万件のデータを高性能にレンダリング初心者でもわかる仮想スクロールの実装方法仮想リストの実装原理の簡単な紹介

以上で、Vue で仮想スクロールを簡単に実装するサンプルコードについての説明は終了です。Vue の仮想スクロールに関するより関連性の高いコンテンツについては、123WORDPRESS.COM の過去の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • vue.js 2.x ベースの仮想スクロールバーのサンプルコード

<<:  centos7.2 オフラインインストール mysql5.7.18.tar.gz

>>:  Win10 での MySQL 5.7 の詳細なインストールと設定のチュートリアル

推薦する

Nginx 外部ネットワーク アクセス イントラネット サイト構成操作

背景:サイトはフロントエンドとバックエンドから分離されています: vue+springbootフロン...

CSS3 弾性拡張ボックスの詳細な説明

使用フレキシブル ボックスはフロントエンドの Web ページ レイアウトで重要な役割を果たしますが、...

Vue プロジェクトで addRoutes を使用する際の問題の解決策

目次序文1. 404 ページ1. 原因2. 解決策2.白い画面を更新する1. 原因2. 解決策3. ...

ウェブサイトに天気予報を挿入する方法

天気予報をウェブサイトに挿入すると、次のような効果が得られます。次のコードを挿入する必要があります:...

mysql5.7.18 解凍バージョンで mysql サービスを起動します

mysql5.7.18の解凍版はmysqlサービスを起動します。具体的な内容は以下のとおりです。 1...

Webデザインにおけるフォームデザインテクニックのまとめ

「脳が多数の領域間の関係を処理できるように、入力は論理的なグループに分割する必要があります。」 – ...

VSCode+CMake+Clang+GCC 環境構築チュートリアル (Win10 の場合)

大学院入試に備えて、C/C++ を使って基本的なデータ構造とアルゴリズムを実装する予定です。アルゴリ...

Mysql ALTER TABLE はフィールドを追加するときにテーブルをロックしますか?

目次MySQL 5.6以前MySQL 5.6以降要約する知らせMySQL 5.6以前更新手順元のテー...

Win7 64 ビット版に MySQL 5.7 をダウンロードしてインストールする際によくある問題の概要

1. 公式ウェブサイトからMySQLをダウンロードします。 これが私たちが探しているものです、win...

MySQL 8.0.19 winx64 インストールチュートリアルと Windows 10 での初期パスワードの変更

この記事では、参考までにMySQL 8.0.19 winx64のインストールチュートリアルを紹介しま...

vue-cli でレスポンシブ レイアウトを実装する方法

フロントエンド開発を行うと、PCとモバイル端末の適応に必然的に直面することになります。このような問題...

npm グローバル モジュールのデフォルトのインストール パスを変更するためにノードのインストールをカスタマイズする手順

node を D ドライブにインストールしましたが、C ドライブのスペースを占有したくなかったため、...

Docker Gitlab+Jenkins+Harborは永続的なプラットフォーム運用を構築します

CI/CD の概要CIワークフロー設計Gitコードバージョン管理システムはコマンドラインでのみ管理で...

HTML テーブル マークアップ チュートリアル (29): セルのライト境界線の色属性 BORDERCOLORLIGHT

セルでは、明るい境界線の色を個別に定義できます。 > 基本構文<TD ボーダーカラーライ...