前言
如何一次性加载并渲染10万条数据,渲染无延时、页面不卡顿?这是面试官经常问到的问题。一开始听到这个问题时,我和很多人一样不屑一顾——这要么是面试官非常闲,要么是后端偷懒没做分页,哪有场景会一次性加载10万条数据,难道不会用分页或者懒加载吗?后来我对股票期货产生了兴趣,发现价格分时数据平均每秒钟有2条,一天下来正好是十几万条数据。这十几万条数据需要一次性获取,并进行数据可视化,既然数据已经到手了,就没有必要再使用分页或者懒加载了。我需要解决的问题就是如何保证用户在滚动十几万条数据时既能流畅查看,又有良好的体验。
一张图了解虚拟长列表原理
图1展示了未使用虚拟长列表的原始状态。假设列表原始长度为15(也可能是数十万),绿色区域是列表的容器,紫色窗口是可看见的区域。无论列表的实际长度是多少,用户只能看到紫色可视窗口中透出的区域。
图2~图5展示了使用虚拟长列表后用户滚动的过程和效果。
在图2中,我们设置列表容器的高度(绿色背景)与图1(未使用虚拟列表)保持一致,但实际上容器中存放的DOM节点只有紫色窗口可以容纳的个数(图中是4个)。通过可视窗口,用户看到的效果和图1是一样的,且滚动条的位置也和图1一致。
当用户滚动一小段距离(如图3所示)后,第1行和第2行的一半被遮盖,第5行之后的空白曝露出来。此时,我们可以获取滚动的距离,根据滚动距离和每行的高度计算出已滚出可视区域的行数。如图4所示,我们需要删除已滚出可视区域的DOM(第1行),同时添加需要展示的DOM(第6行)。在删除隐藏的DOM后,我们通过设置绿色容器的上padding来填充删除的DOM所留下的空缺(如图5中灰色部分所示),从而确保透过可视区域的DOM节点在视觉上没有位置变动。
通过图3、4、5的操作,用户的视觉效果(紫色框内部分)和操作体验与不使用虚拟长列表的图1基本一致,但实际上只有紫色窗口内的寥寥数个真实DOM节点存在。
如果使用Vue或React,只需对隐藏的DOM进行删除,并添加需要展示的DOM即可(例如,将[1,2,3,4,5]替换为[2,3,4,5,6])。
Vue3 实现虚拟长列表
<div id="app"></div>
<script src="https://unpkg.com/vue@next"></Script>
<script>
let t = Date.now()
const { ref, reactive, computed, onMounted, createApp } = Vue;
createApp({
template: `
<div class="container" @scroll="onScroll">
<div class="panel" ref="panel"
:style="{paddingTop: paddingTop + 'px'}">
<div class="item" v-for="item in visibleList" :key="item">
{{ item }}
</div>
</div>
</div>`,
setup() {
let panel = ref(null) //列表容器DOM
//构造的长列表原始数据
let raw = Array(100000).fill(0).map((v, i) => `item-${i}`);
let count = 10; //实际渲染DOM的列表数量
let start = ref(0); //从长列表数组总截取数据的起点
let end = ref(10); //从长列表数组总截取数据的终点
let itemHeight = 30; //单个列表项的高度
let paddingTop = ref(0); //列表容器的上内边距
let totalHeight = raw.length*itemHeight //原始数据理论上完全渲染后占据的总高度
let visibleList = computed(() => raw.slice(start.value, end.value)); //根据起点和终点获取要渲染的数据
onMounted(() => panel.value.style.height = totalHeight + 'px') //在mounted后设置列表容器的高度
//滚动-->根据滚动距离计算起点和终点的下标-->计算属性得到visibleList-->真实DOM被替换 同时设置paddingTop让元素视觉上没跳动
const onScroll = function (e) {
start.value = Math.floor(e.target.scrollTop / itemHeight); //当滚动后,重新计算起点的位置
end.value = start.value + count; //设置终点的位置
paddingTop.value = start.value*itemHeight;
};
return {
visibleList, paddingTop, panel, onScroll
};
}
}).mount('#app');
</Script>
<style>
* {
box-sizing: border-box;
margin: 0;
}
.container {
height: 300px;
overflow-y: scroll;
}
.item {
border: 1px solid #eee;
line-height: 30px;
height: 30px;
padding: 0 10px;
cursor: pointer;
}
</style>
优化
以上几行核心代码实现了虚拟长列表,但还存在以下问题:
可视区域高度、展示的列表数量和子项的高度都是固定的,使用起来不太方便。 滚动时触发渲染的频率太高,导致滚动时不够流畅(在移动端更明显)。 快速滚动时可能会出现短暂的白屏现象,可以对原始DOM进行进一步优化。
可以根据可视区域的高度自动计算展示列表的数量,并且列表子项的高度不固定。对滚动事件进行防抖或者节流处理,减少计算的频率。
第二点优化可以提升性能,但会延长滚动时上下方出现空白的时间。可以通过添加列表缓冲处理(例如,可视区域展示10个列表,上方隐藏10个列表,下方隐藏10个列表)来解决这个问题。