背景
小程序在很多场景下面会遇到长列表的交互,当一个页面渲染过多的wxml节点的时候,会造成小程序页面的卡顿和白屏。原因主要有以下几点:
- 列表数据量大,初始化setData和初始化渲染列表wxml耗时都比较长;
- 渲染的wxml节点比较多,每次setData更新视图都需要创建新的虚拟树,和旧树的diff操作耗时比较高;
- 渲染的wxml节点比较多,page能够容纳的wxml是有限的,占用的内存高。
微信小程序本身的scroll-view没有针对长列表做优化,官方组件recycle-view就是一个类似virtual-list的长列表组件。现在我们要剖析虚拟列表的原理,从零实现一个小程序的virtual-list。
实现原理
首先我们要了解什么是virtual-list,这是一种初始化只加载「可视区域」及其附近dom元素,并且在滚动过程中通过复用dom元素只渲染「可视区域」及其附近dom元素的滚动列表前端优化技术。相比传统的列表方式可以到达极高的初次渲染性能,并且在滚动过程中只维持超轻量的dom结构。
虚拟列表最重要的几个概念:
-
可滚动区域:比如列表容器的高度是600,内部元素的高度之和超过了容器高度,这一块区域就可以滚动,就是「可滚动区域」;
-
可视区域:比如列表容器的高度是600,右侧有纵向滚动条可以滚动,视觉可见的内部区域就是「可视区域」。
实现虚拟列表的核心就是监听scroll事件,通过滚动距离offset和滚动的元素的尺寸之和totalSize动态调整「可视区域」数据渲染的顶部距离和前后截取索引值,实现步骤如下:
- 监听scroll事件的scrollTop/scrollLeft,计算「可视区域」起始项的索引值startIndex和结束项索引值endIndex;
- 通过startIndex和endIndex截取长列表的「可视区域」的数据项,更新到列表中;
- 计算可滚动区域的高度和item的偏移量,并应用在可滚动区域和item上。
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ÿ