ホームページ  >  記事  >  WeChat アプレット  >  WeChat アプレットに仮想リストを実装する方法の詳細な説明

WeChat アプレットに仮想リストを実装する方法の詳細な説明

coldplay.xixi
coldplay.xixi転載
2020-09-07 15:48:517053ブラウズ

WeChat アプレットに仮想リストを実装する方法の詳細な説明

【関連する学習の推奨事項: WeChat ミニ プログラム チュートリアル ]

背景

ミニ プログラムでは、多くのシナリオでインタラクションの長いリストが発生します。ページでレンダリングされる wxml ノードが多すぎると、ミニ プログラム ページがフリーズして画面が白くなります。主な理由としては、

1. リストデータ量が多く、setDataの初期化やレンダリングリストwxmlの初期化に時間がかかる、

2. が多いwxml ノードがレンダリングされ、そのたびに SetData がビューを更新するために新しい仮想ツリーを作成する必要があり、古いツリーの差分操作には比較的時間がかかります;

3. レンダリングされる wxml ノードが多数あります。ページに収容できる wxml は限られており、メモリ占有量は多くなります。

WeChat アプレット自体のスクロールビューは長いリスト用に最適化されていないため、公式コンポーネントの recycle-view は virtual-list に似た長いリスト コンポーネントです。ここで、仮想リストの原理を分析し、小さなプログラムの仮想リストを最初から実装してみます。

実装原理

まず第一に、virtual-list とは何かを理解する必要があります。これは、「可視領域」とその近くの dom 要素のみをロードし、実行中にそれらを再利用する初期化です。スクロール プロセス: 「表示領域」とその近くの DOM 要素のみをレンダリングするスクロール リストのフロントエンド最適化テクノロジ。従来のリスト方式と比較して、非常に高い初期レンダリング パフォーマンスを実現でき、スクロール処理中のみ超軽量の DOM 構造を維持します。

仮想リストの最も重要な概念:

  • スクロール可能な領域: たとえば、リスト コンテナーの高さは 600 で、内部コンテナーの高さの合計は 600 です。要素がコンテナの高さを超えています。この領域はスクロールできます。これは「スクロール可能領域」です。

  • 可視領域: たとえば、リスト コンテナの高さは 600 で、右側にはスクロール用の垂直スクロール バーがあり、視覚的に表示されます。内部領域は「視覚領域」です。

仮想リストの実装の核心は、スクロール イベントをリッスンし、スクロール距離オフセットを通じて上部の距離と「視覚領域」データ レンダリングの前後のインターセプトを動的に調整することです。およびスクロールされた要素のサイズの合計 totalSize インデックス値、実装手順は次のとおりです:

1. スクロール イベントのscrollTop/scrollLeftをリッスンし、スクロール イベントのインデックス値startIndexを計算します。 「可視領域」の開始項目と終了項目のインデックス値 endIndex;

2 .startIndex と endIndex を通じて長いリストの「可視領域」のデータ項目をインターセプトし、それらをlist;

3. スクロール可能領域の高さと項目のオフセットを計算し、スクロール可能領域と項目に適用します。

WeChat アプレットに仮想リストを実装する方法の詳細な説明
#1. リスト項目の幅/高さおよびスクロール オフセット

仮想リストでは、各リストによって異なります。 item 幅/高さは「スクロール可能領域」の計算に使用され、カスタマイズが必要になる場合があります。リスト項目の幅/高さを計算するには itemSizeGetter 関数を定義します。

itemSizeGetter(itemSize) {      return (index: number) => {        if (isFunction(itemSize)) {          return itemSize(index);
        }        return isArray(itemSize) ? itemSize[index] : itemSize;
      };
    }复制代码

スクロール処理中、表示されていないリスト項目の itemSize は計算されません。このとき、推定リスト項目estimatedItemSizeが使用されます。目的は、「」の高さを計算することです。渡された itemSize は、estimatedItemSize に置き換えられます。

getSizeAndPositionOfLastMeasuredItem() {    return this.lastMeasuredIndex >= 0
      ? this.itemSizeAndPositionData[this.lastMeasuredIndex]
      : { offset: 0, size: 0 };
  }

getTotalSize(): number {    const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem();    return (
      lastMeasuredSizeAndPosition.offset +
      lastMeasuredSizeAndPosition.size +
      (this.itemCount - this.lastMeasuredIndex - 1) * this.estimatedItemSize
    );
  }复制代码

ここでは、最後に計算されたリスト項目の itemSize と offset がキャッシュを通じて直接ヒットしていることがわかります。これは、各リスト項目の 2 つのパラメーターが取得されるときに、それらがキャッシュされるためです。

 getSizeAndPositionForIndex(index: number) {    if (index > this.lastMeasuredIndex) {      const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem();      let offset =
        lastMeasuredSizeAndPosition.offset + lastMeasuredSizeAndPosition.size;      for (let i = this.lastMeasuredIndex + 1; i <= index; i++) {        const size = this.itemSizeGetter(i);        this.itemSizeAndPositionData[i] = {
          offset,
          size,
        };

        offset += size;
      }      this.lastMeasuredIndex = index;
    }    return this.itemSizeAndPositionData[index];
 }复制代码

2. オフセットに基づいたインデックス値の検索

スクロール処理中、スクロールを通じて「表示領域」に表示される最初のデータのインデックス値を計算する必要があります。 offset offset. 通常、各リスト項目の itemSize は 0 から計算され、offset を超えるとインデックス値が取得されます。ただし、データ量が多すぎてスクロール イベントが頻繁にトリガーされると、パフォーマンスが大幅に低下します。幸いなことに、リスト項目のスクロール距離は完全に昇順に配置されているため、キャッシュされたデータに対して二分検索を実行して、時間の複雑さを O(lgN) に減らすことができます。

js コードは次のとおりです。

  findNearestItem(offset: number) {
    offset = Math.max(0, offset);    const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem();    const lastMeasuredIndex = Math.max(0, this.lastMeasuredIndex);    if (lastMeasuredSizeAndPosition.offset >= offset) {      return this.binarySearch({        high: lastMeasuredIndex,        low: 0,
        offset,
      });
    } else {      return this.exponentialSearch({        index: lastMeasuredIndex,
        offset,
      });
    }
  }

 private binarySearch({
    low,
    high,
    offset,
  }: {    low: number;
    high: number;
    offset: number;
  }) {    let middle = 0;    let currentOffset = 0;    while (low <= high) {
      middle = low + Math.floor((high - low) / 2);
      currentOffset = this.getSizeAndPositionForIndex(middle).offset;      if (currentOffset === offset) {        return middle;
      } else if (currentOffset < offset) {
        low = middle + 1;
      } else if (currentOffset > offset) {
        high = middle - 1;
      }
    }    if (low > 0) {      return low - 1;
    }    return 0;
  }复制代码

計算結果をキャッシュしない検索の場合は、まず指数関数検索で検索範囲を絞り、次に二分検索を使用します。

private exponentialSearch({
    index,
    offset,
  }: {    index: number;
    offset: number;
  }) {    let interval = 1;    while (
      index < this.itemCount &&      this.getSizeAndPositionForIndex(index).offset < offset
    ) {
      index += interval;
      interval *= 2;
    }    return this.binarySearch({      high: Math.min(index, this.itemCount - 1),      low: Math.floor(index / 2),
      offset,
    });
  }
}复制代码

3. startIndex と endIndex を計算します

「視覚領域」containerSize、ローリング オフセット offset のサイズがわかっているので、事前にレンダリングされたバーの数 overscanCount を追加して調整します。 「可視領域」の開始項目のインデックス値 startIndex と終了項目のインデックス値 endIndex を計算します。実装手順は次のとおりです:

1. オフセットに最も近いインデックス値を見つけます。この値開始項目のインデックス値です。startIndex;

2. startIndex を通じて項目のオフセットとサイズを取得し、オフセットを調整します。

3. オフセットをcontainerSizeに追加して、終了項目の maxOffset を取得し、startIndex から maxOffset を超えて終了項目のインデックス値 endIndex を取得するまで累積を開始します。

js コードは次のとおりです:

 getVisibleRange({
    containerSize,
    offset,
    overscanCount,
  }: {    containerSize: number;
    offset: number;
    overscanCount: number;
  }): { start?: number; stop?: number } {    const maxOffset = offset + containerSize;    let start = this.findNearestItem(offset);    const datum = this.getSizeAndPositionForIndex(start);
    offset = datum.offset + datum.size;    let stop = start;    while (offset < maxOffset && stop < this.itemCount - 1) {
      stop++;
      offset += this.getSizeAndPositionForIndex(stop).size;
    }    if (overscanCount) {
      start = Math.max(0, start - overscanCount);
      stop = Math.min(stop + overscanCount, this.itemCount - 1);
    }    return {
      start,
      stop,
    };
}复制代码

3. 仮想リストのスクロールを実現するためにスクロール イベントをリッスンします

startIndex、endIndex、totalSize、およびを動的に更新できるようになりました。スクロールイベントをリッスンすることでオフセットを取得し、仮想リストスクロールを実現できます。

js コードは次のとおりです:

  getItemStyle(index) {      const style = this.styleCache[index];      if (style) {        return style;
      }      const { scrollDirection } = this.data;      const {
        size,
        offset,
      } = this.sizeAndPositionManager.getSizeAndPositionForIndex(index);      const cumputedStyle = styleToCssString({        position: &#39;absolute&#39;,        top: 0,        left: 0,        width: &#39;100%&#39;,
        [positionProp[scrollDirection]]: offset,
        [sizeProp[scrollDirection]]: size,
      });      this.styleCache[index] = cumputedStyle;      return cumputedStyle;
  },
  
  observeScroll(offset: number) {      const { scrollDirection, overscanCount, visibleRange } = this.data;      const { start, stop } = this.sizeAndPositionManager.getVisibleRange({        containerSize: this.data[sizeProp[scrollDirection]] || 0,
        offset,
        overscanCount,
      });      const totalSize = this.sizeAndPositionManager.getTotalSize();      if (totalSize !== this.data.totalSize) {        this.setData({ totalSize });
      }      if (visibleRange.start !== start || visibleRange.stop !== stop) {        const styleItems: string[] = [];        if (isNumber(start) && isNumber(stop)) {          let index = start - 1;          while (++index <= stop) {
            styleItems.push(this.getItemStyle(index));
          }
        }        this.triggerEvent(&#39;render&#39;, {          startIndex: start,          stopIndex: stop,
          styleItems,
        });
      }      this.data.offset = offset;      this.data.visibleRange.start = start;      this.data.visibleRange.stop = stop;
  },复制代码

在调用的时候,通过render事件回调出来的startIndex, stopIndex,styleItems,截取长列表「可视区域」的数据,在把列表项目的itemSize和offset通过绝对定位的方式应用在列表上

代码如下:

let list = Array.from({ length: 10000 }).map((_, index) => index);

Page({  data: {    itemSize: index => 50 * ((index % 3) + 1),    styleItems: null,    itemCount: list.length,    list: [],
  },
  onReady() {    this.virtualListRef =      this.virtualListRef || this.selectComponent(&#39;#virtual-list&#39;);
  },

  slice(e) {    const { startIndex, stopIndex, styleItems } = e.detail;    this.setData({      list: list.slice(startIndex, stopIndex + 1),
      styleItems,
    });
  },

  loadMore() {
    setTimeout(() => {      const appendList = Array.from({ length: 10 }).map(        (_, index) => list.length + index,
      );
      list = list.concat(appendList);      this.setData({        itemCount: list.length,        list: this.data.list.concat(appendList),
      });
    }, 500);
  },
});复制代码
<view class="container">
  <virtual-list scrollToIndex="{{ 16 }}" lowerThreshold="{{50}}" height="{{ 600 }}" overscanCount="{{10}}" item-count="{{ itemCount }}" itemSize="{{ itemSize }}" estimatedItemSize="{{100}}" bind:render="slice" bind:scrolltolower="loadMore">
    <view wx:if="{{styleItems}}">
      <view wx:for="{{ list }}" wx:key="index" style="{{ styleItems[index] }};line-height:50px;border-bottom:1rpx solid #ccc;padding-left:30rpx">{{ item + 1 }}</view>
    </view>
  </virtual-list>
  {{itemCount}}</view>复制代码
WeChat アプレットに仮想リストを実装する方法の詳細な説明

参考资料

在写这个微信小程序的virtual-list组件过程中,主要参考了一些优秀的开源虚拟列表实现方案:

  • react-tiny-virtual-list
  • react-virtualized
  • react-window

总结

通过上述解释已经初步实现了在微信小程序环境中实现了虚拟列表,并且对虚拟列表的原理有了更加深入的了解。但是对于瀑布流布局,列表项尺寸不可预测等场景依然无法适用。在快速滚动过程中,依然会出现来不及渲染而白屏,这个问题可以通过增加「可视区域」外预渲染的item条数overscanCount来得到一定的缓解。

想了解更多编程学习,敬请关注php培训栏目!

以上がWeChat アプレットに仮想リストを実装する方法の詳細な説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.imで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。