长列表优化-虚拟列表(Vue3 + Ts + SFC)

1,521 阅读3分钟

场景

以H5(Web同理)中新闻列表为例,当前端展示列表时,一般都会做分页处理,当在页面上上拉时加载下一页。理想情况下我们可以一下上拉加载下一页。

这时就会产生一个问题,数据较少滚动会很流畅,随着数据的增多,页面的滚动会变得越来越卡顿。

分析问题

很明显出现这种问题的原因是:随着数据的增多,页面上的dom元素也就越多,从而导致页面渲染的速度变慢,造成卡顿。那么如何解决呢?

首先我们知道手机屏幕是有一定宽高的,那么们能不能只渲染当前屏幕的数据,其它数据用空白来展示呢?答案是只可以的,这就是我们今天要说的虚拟列表。

组件实现

  1. 首先,创建一个可以滚动的容器,并在容器中添加一个内容容器用来显示内容,并在这个内容容器中添加一个slot,代码如下:
    <div class="v-list-container" ref="listContainer" @scroll="handleScroll">
        <div class="list-container" :style="listStyle">
            <slot></slot>
        </div>
    </div>
  1. 其次,要判断一屏幕最多显示几条数据,如下图所示,假设一条数据高度为itemHeight = 160,手机屏幕最多显示size = 6条数据。另外需要知道数据的总长度dataLength(可动态变化) 虚拟列表 (1).png

主要代码:

const props = defineProps({
    // 每行元素高度
    itemHeight: {
        type: Number,
        default: 160
    },
    // 每屏显示条数
    size: {
        type: Number,
        default: 6,
    },
    // 总数据长度
    dataLength: {
        type: Number,
        default: 0,
    },
})
  1. 计算数据的起始索引,用以截取数据并渲染。默认设置开始索引为startIndex = 0,结束索引为endIndex = startIndex + props.size,因为每屏显示的数据条数是固定的,所以我们可以用计算属性来自动计算endIndex
// 开始索引
const startIndex = ref(0);
// 结束索引
const endIndex = computed(() => {
    return index + props.size;
});
  1. 为容器添加handleScroll事件,监听面滚动并自动计算开始索引。
// 通过ref获取dom元素
const listContainer = ref<HTMLElement | null>(null);
// 容器滚动
const handleScroll = () => {
    window.requestAnimationFrame(() => {
        const scrollTop = listContainer.value?.scrollTop || 0;
        startIndex.value = Math.floor(scrollTop / props.itemHeight);
    });
}
  1. 改变内容容器的padding-toppadding-bottom来实现内容容器高度不变,以空白代替页面元素的效果。
// 计算样式
const listStyle = computed(() => {
    const bottom = props.dataLength - endIndex.value;
    return {
        paddingTop: startIndex.value * props.itemHeight + 'px',
        paddingBottom: bottom * props.itemHeight + 'px'
    }
});
  1. 监听startIndexendIndex的变化,并将它们的值传递给父组件。父组件接收到数据后,会截取相应的数据,并进行渲染。
const emit = defineEmits(['indexChange']);
watch([startIndex, endIndex], (val) => {
    let index = 0;
    const startIndex = val[0];
    if (startIndex > props.size) {
        index = startIndex;
    }
    emit('indexChange', index, val[1]);
});
  1. 监听数据长度dataLength的变化。如果我们重新请求了第一页的数据,需要重新设置开始索引startIndex
// 重置startIndex
watch(() => props.dataLength, (val, old) => {
    if (val < old) {
        startIndex.value = 0;
    }
})

问题

到这里,基本的虚拟列表功能已经实现,但是还有一个小问题,就是当你快速滑动列表的时候,页面会先出现空白,然后才会渲染出页面元素。为了解决这个问题,我们可以使用预渲染,提前加载上一屏及下一屏的数据。如下图所示,不同于之前只显示6条数据,这里我们会加载18条数据:

虚拟列表.png

const props = defineProps({
    // ……
    // 预加载n屏数据
    page: {
        type: Number,
        default: 1
    }
})

相应的,原本的endIndexlistStyle及其它相关代码都因做出相应改变,这里就不详细展示了。下面有完整的代码及demo。

Github代码

github.com/xzzfxz/lc-u…