虚拟列表

Friday, March 11, 2022

背景

业务上需要展示较多数据,直接渲染在部分机型上滑动出现卡顿,故使用虚拟列表进行优化。

原理

简单来说,虚拟列表是指只渲染用户屏幕可视区域内部分内容(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>
  1. v-container是可视区域的容器,具有overflow-y: auto属性
  2. list-container是模拟长列表的容器,为了还原高度以及模拟正常长列表滚动

通过v-container的滚动监听函数,我们可以获得当前滚动的scrollTop,并以此来计算startIndexendIndex

  1. 计算前,我们需要提前定义部分数值:

    1. rowHeight 列表内单行的元素高度
    2. total 列表内总共行数
    3. height 用户的可视区域高度
  2. 依靠上面的变量,我们可以计算出下列数据:

    1. listHeight = total * rowHeight 长列表正常高度
    2. 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
    }
  }

有了startIndexendIndex ,我们就能渲染出当前滚动高度内的“正确”内容,但除此外我们还需要将内容“摆”在height-container的”正确“位置,为此可以这样解决:

  1. height-container添加上position: relative样式,作为子节点(内容)的锚点
  2. 为内容item添加上position: absolute样式,再根据indexrowHeight计算出与顶部间距(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
    }
  }

代码片段