场景
以H5(Web同理)中新闻列表为例,当前端展示列表时,一般都会做分页处理,当在页面上上拉时加载下一页。理想情况下我们可以一下上拉加载下一页。
这时就会产生一个问题,数据较少滚动会很流畅,随着数据的增多,页面的滚动会变得越来越卡顿。
分析问题
很明显出现这种问题的原因是:随着数据的增多,页面上的dom元素也就越多,从而导致页面渲染的速度变慢,造成卡顿。那么如何解决呢?
首先我们知道手机屏幕是有一定宽高的,那么们能不能只渲染当前屏幕的数据,其它数据用空白来展示呢?答案是只可以的,这就是我们今天要说的虚拟列表。
组件实现
- 首先,创建一个可以滚动的容器,并在容器中添加一个内容容器用来显示内容,并在这个内容容器中添加一个
slot
,代码如下:
<div class="v-list-container" ref="listContainer" @scroll="handleScroll">
<div class="list-container" :style="listStyle">
<slot></slot>
</div>
</div>
- 其次,要判断一屏幕最多显示几条数据,如下图所示,假设一条数据高度为
itemHeight = 160
,手机屏幕最多显示size = 6
条数据。另外需要知道数据的总长度dataLength
(可动态变化)
主要代码:
const props = defineProps({
// 每行元素高度
itemHeight: {
type: Number,
default: 160
},
// 每屏显示条数
size: {
type: Number,
default: 6,
},
// 总数据长度
dataLength: {
type: Number,
default: 0,
},
})
- 计算数据的起始索引,用以截取数据并渲染。默认设置开始索引为
startIndex = 0
,结束索引为endIndex = startIndex + props.size
,因为每屏显示的数据条数是固定的,所以我们可以用计算属性来自动计算endIndex
。
// 开始索引
const startIndex = ref(0);
// 结束索引
const endIndex = computed(() => {
return index + props.size;
});
- 为容器添加
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);
});
}
- 改变内容容器的
padding-top
和padding-bottom
来实现内容容器高度不变,以空白代替页面元素的效果。
// 计算样式
const listStyle = computed(() => {
const bottom = props.dataLength - endIndex.value;
return {
paddingTop: startIndex.value * props.itemHeight + 'px',
paddingBottom: bottom * props.itemHeight + 'px'
}
});
- 监听
startIndex
和endIndex
的变化,并将它们的值传递给父组件。父组件接收到数据后,会截取相应的数据,并进行渲染。
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]);
});
- 监听数据长度
dataLength
的变化。如果我们重新请求了第一页的数据,需要重新设置开始索引startIndex
。
// 重置startIndex
watch(() => props.dataLength, (val, old) => {
if (val < old) {
startIndex.value = 0;
}
})
问题
到这里,基本的虚拟列表功能已经实现,但是还有一个小问题,就是当你快速滑动列表的时候,页面会先出现空白,然后才会渲染出页面元素。为了解决这个问题,我们可以使用预渲染,提前加载上一屏及下一屏的数据。如下图所示,不同于之前只显示6条数据,这里我们会加载18条数据:
const props = defineProps({
// ……
// 预加载n屏数据
page: {
type: Number,
default: 1
}
})
相应的,原本的endIndex
和listStyle
及其它相关代码都因做出相应改变,这里就不详细展示了。下面有完整的代码及demo。