搜尋
首頁微信小程式小程式開發在微信小程式中實作virtual-list的方法詳解

在微信小程式中實作virtual-list的方法詳解

【相關學習推薦:微信小程式教學

背景

小程式在很多場景下面會遇到長列表的交互,當一個頁面渲染過多的wxml節點的時候,會造成小程式頁面的卡頓和白屏。原因主要有以下幾點:

1.列表資料量大,初始化setData和初始化渲染列表wxml耗時都比較長;

2.渲染的wxml節點比較多,每次setData更新視圖都需要建立新的虛擬樹,和舊樹的diff操作耗時比較高;

#3.渲染的wxml節點比較多,page能夠容納的wxml是有限的,佔用的記憶體高。

微信小程式本身的scroll-view並沒有針對長列表做優化,官方元件recycle-view就是一個類似virtual-list的長列表元件。現在我們要剖析虛擬列表的原理,從零實作一個小程式的virtual-list。

實作原理

首先我們要了解什麼是virtual-list,這是一種初始化只載入「視覺區域」及其附近dom元素,並且在捲動過程中透過重複使用dom元素只渲染「視覺區域」及其附近dom元素的捲動清單前端優化技術。相較於傳統的列表方式可以到達極高的初次渲染性能,並且在滾動過程中只維持超輕量的dom結構。

虛擬清單最重要的幾個概念:

  • 可捲動區域:例如清單容器的高度是600,內部元素的高度總和超過了容器高度,這一塊區域就可以滾動,就是「可滾動區域」;

  • 視覺區域:例如清單容器的高度是600,右邊有縱向捲軸可以滾動,視覺可見的內部區域就是「可視區域」。

實現虛擬清單的核心就是監聽scroll事件,透過滾動距離offset和滾動的元素的尺寸總和totalSize動態調整「視覺區域」資料渲染的頂部距離和前後截取索引值,實作步驟如下:

1.監聽scroll事件的scrollTop/scrollLeft,計算「視覺區域」起始項的索引值startIndex和結束項索引值endIndex;

2 .透過startIndex和endIndex截取長列表的「視覺區域」的資料項,更新到清單中;

3.計算可捲動區域的高度和item的偏移量,並套用在可捲動區域和item上。

在微信小程式中實作virtual-list的方法詳解

1.列表項的寬/高和滾動偏移量

在虛擬列表中,依賴每一個列表項的寬/高來計算“可滾動區域”,而且可能是需要自訂的,定義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,這是因為在取得每一個清單項目的兩個參數時候,都對其做了快取。

 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計算出展示在「視覺區域」首項資料的索引值,一般情況下可以從0開始計算每個清單項目的itemSize,累加到一旦超過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.找到距離offset最近的索引值,這個值就是起始項的索引值startIndex;

2.透過startIndex取得此項目的offset和size,再對offset進行調整;

3.offset加上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.監聽scroll事件,實作虛擬清單滾動

現在可以透過監聽scroll事件,動態更新startIndex、endIndex、totalSize、offset,就可以實現虛擬列表滾動。

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>复制代码
在微信小程式中實作virtual-list的方法詳解

参考资料

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

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

总结

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

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

以上是在微信小程式中實作virtual-list的方法詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文轉載於:juejin。如有侵權,請聯絡admin@php.cn刪除

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
4 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
4 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
4 週前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
1 個月前By尊渡假赌尊渡假赌尊渡假赌

熱工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具

WebStorm Mac版

WebStorm Mac版

好用的JavaScript開發工具

Atom編輯器mac版下載

Atom編輯器mac版下載

最受歡迎的的開源編輯器

Dreamweaver Mac版

Dreamweaver Mac版

視覺化網頁開發工具