【関連する学習の推奨事項: 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. スクロール可能領域の高さと項目のオフセットを計算し、スクロール可能領域と項目に適用します。
#1. リスト項目の幅/高さおよびスクロール オフセット仮想リストでは、各リストによって異なります。 item 幅/高さは「スクロール可能領域」の計算に使用され、カスタマイズが必要になる場合があります。リスト項目の幅/高さを計算するには itemSizeGetter 関数を定義します。itemSizeGetter(itemSize) { return (index: number) => { if (isFunction(itemSize)) { return itemSize(index); } return isArray(itemSize) ? itemSize[index] : itemSize; }; }复制代码
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 ); }复制代码
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]; }复制代码
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, }); } }复制代码
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, }; }复制代码
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: 'absolute', top: 0, left: 0, width: '100%', [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('render', { 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('#virtual-list'); }, 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>复制代码
在写这个微信小程序的virtual-list组件过程中,主要参考了一些优秀的开源虚拟列表实现方案:
通过上述解释已经初步实现了在微信小程序环境中实现了虚拟列表,并且对虚拟列表的原理有了更加深入的了解。但是对于瀑布流布局,列表项尺寸不可预测等场景依然无法适用。在快速滚动过程中,依然会出现来不及渲染而白屏,这个问题可以通过增加「可视区域」外预渲染的item条数overscanCount来得到一定的缓解。
想了解更多编程学习,敬请关注php培训栏目!
以上がWeChat アプレットに仮想リストを実装する方法の詳細な説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。