在微信小程序中实现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张图片

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.根据偏移量搜索索引值

在滚动过程中,需要

你可能感兴趣的:(小程序)