背景
业务上需要展示较多数据,直接渲染在部分机型上滑动出现卡顿,故使用虚拟列表进行优化。
原理
简单来说,虚拟列表是指只渲染用户屏幕可视区域内部分内容(item-4 ~ item-13)。
与将所有数据渲染出各自dom节点对比,只渲染可视区域的形式可以大幅度优化我们的页面流畅度。
实现
首先为了实现只渲染可视区域这一目标,我们需要知道可视区内开始与结束的节点。以上图为例,就是item-4与item-13,用以下参数表示:
1. `startIndex` 可见区域的开始节点
2. `endIndex` 可见区域的结束节点
因为我们是选择性渲染,所以撑起的容器高度大概率是小于普通长列表容器高度的,为了保持两者的一致,HTML结构设计如下:
小程序内监听滑动需用到scroll-view组件,web中均用div即可
<scroll-view class="v-container">
<view class="list-container">
...
<!-- item-1 -->
<!-- item-2 -->
<!-- item-3 -->
....
</view>
</scroll-view>
v-container
是可视区域的容器,具有overflow-y: auto
属性list-container
是模拟长列表的容器,为了还原高度以及模拟正常长列表滚动
通过v-container
的滚动监听函数,我们可以获得当前滚动的scrollTop
,并以此来计算startIndex
与endIndex
-
计算前,我们需要提前定义部分数值:
rowHeight
列表内单行的元素高度total
列表内总共行数height
用户的可视区域高度
-
依靠上面的变量,我们可以计算出下列数据:
listHeight = total * rowHeight
长列表正常高度limit = Math.ceil(height/rowHeight)
可视区域内可展示最多元素数量
配合滚动监听函数onScroll
,我们可以进行以下计算:
onScroll(e) {
const scrollTop = e.detail.scrollTop
const { startIndex, total, rowHeight, limit } = this.privateData
// 计算当前可视区域内首个元素index
const currentStartIndex = Math.floor(scrollTop / rowHeight)
// 与上次index不相等,即需要更新
if (currentStartIndex !== startIndex) {
this.privateData.startIndex = currentStartIndex
// endIndex最大为当前列表最后一行
this.privateData.endIndex = Math.min(currentStartIndex + limit, total - 1
}
}
有了startIndex
与endIndex
,我们就能渲染出当前滚动高度内的“正确”内容,但除此外我们还需要将内容“摆”在height-container
的”正确“位置,为此可以这样解决:
- 为
height-container
添加上position: relative
样式,作为子节点(内容)的锚点 - 为内容
item
添加上position: absolute
样式,再根据index
与rowHeight
计算出与顶部间距(top
)
代码如下:
renderDisplayContent() {
const { rowHeight, startIndex, endIndex } = this.privateData
const content = []
for (let i = startIndex; i <= endIndex; i++) {
content.push({
index: i,
style: `
width: 100%;
position: absolute;
top: ${i * rowHeight}rpx;
`
})
}
this.setData({ content })
}
基于以上实现的虚拟列表,在快速滚动时会出现闪烁白屏现象,为了解决这个问题,我们可以再增加一个参数
1. bufferSize
除可视区域外,上下过渡渲染行数
加入该参数后,我们渲染的内容就等于上bufferSize + 最多可视条数 + 下bufferSize,通俗点就是增加前后内容长度弥补来不及渲染的问题,onScroll
修改如下:
onScroll(e) {
const scrollTop = e.detail.scrollTop
const { startIndex, total, bufferSize, rowHeight, limit } = this.privateData
const currentStartIndex = Math.floor(scrollTop / rowHeight)
if (currentStartIndex !== startIndex) {
// 前后计算加入bufferSize
this.privateData.startIndex = Math.max(currentIndex - bufferSize, 0)
this.privateData.endIndex = Math.min(currentStartIndex + limit + bufferSize, total) - 1
}
}