后端一次性给了 10W 数据,我该如何渲染?
这是一个经典的考察虚拟列表的问题,那么到底什么是虚拟列表?
原理探究
虚拟列表是一种优化长列表性能的技术。它通过只渲染当前可见的区域,而不是将整个列表都渲染出来的方式,减小了页面渲染的负担,从而提高了长列表的滚动和渲染性能。
具体地说,在虚拟列表中,我们只渲染当前可见的一部分列表项,并根据用户滚动或其他操作进行动态调整。例如,对于一个包含1000个列表项的列表,如果当前显示10个列表项,则我们只渲染10个列表项的DOM元素,其他元素则暂时不进行渲染。
当用户滚动列表时,我们根据滚动位置实时计算出需要渲染的列表项,并更新页面上的DOM元素。这样,即便列表非常长,我们也可以保证页面的渲染性能,同时还能提供流畅的滚动体验。
简单来说,虚拟列表就是用 JS 控制渲染的列表项,来避免大规模的 DOM 渲染带来的性能消耗。
其实现原理如下
实战操作
在实现虚拟列表之前,先来看一次性渲染 10W DOM 的性能,对比一下两种渲染方式的性能。
以 Vue3 为例,我们可以这样实现(可以将很多变量提取为 props, 这里就不搞那么麻烦了)
<template>
<div
@scroll="handleScroll"
:style="{
position: 'relative',
overflowY: 'auto',
height: visibleHeight + 'px',
width: '300px',
}"
>
<div :style="{ height: containerHeight + 'px' }"></div>
<div
:style="{
position: 'absolute',
top: startOffset + 'px',
height: visibleHeight + 'px',
}"
>
<div
v-for="item in visibleData"
:key="item"
:style="{ height: rowHeight + 'px' }"
>
Row {{ item }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from "vue";
const rowCount = 100000; // 总数据量
const rowHeight = 50; // 单行高度
const visibleHeight = 400; // 可见范围内数据的总高度
const containerHeight = rowCount * rowHeight; // 容器高度
const scrollTop = ref(0); // 虚拟列表距离容器顶部的距离
// 计算可见范围内数据的起始索引和结束索引
const startIndex = computed(() => Math.floor(scrollTop.value / rowHeight));
const endIndex = computed(() =>
Math.min(Math.ceil((scrollTop.value + visibleHeight) / rowHeight), rowCount)
);
// 计算可见范围内的数据
const visibleData = computed(() =>
Array.from(
{ length: endIndex.value - startIndex.value },
(_, index) => startIndex.value + index + 1
)
);
// 监听滚动事件,并更新 scrollTop 属性
const handleScroll = (e) => {
scrollTop.value = e.target.scrollTop;
};
</script>
其中涉及到的几个变量说明如下:
- rowCount:数据量统计,真实场景下是根据数据计算出来的;
- rowHeight:单行高度;
- visibleHeight:可见高度,即虚拟列表的高度;
- scrollTop:虚拟列表数据展示部分距离顶部的高度,需要让数据渲染部分始终位于可见区域;
- startIndex:数据起始坐标;
- endIndex:数据结束坐标
- containerHeight:所有数据的高度总和,为了渲染真实的滚动条;
- visibleData:可见区域的数据。
效果如下
其 DOM 提现如下
可以看到页面上元素的个数是固定几条,并没有把全部的数据都渲染出来,此时的首次渲染性能分析如下
可以看到渲染时间大大减少。
此时的虚拟列表还是有点僵硬,滚动起来不自然,我们需要为其添加更加自然的滚动效果。
因为我们的虚拟列表的绝对定位高度始终和容器的滚动距离一致,在视觉上就体现为虚拟列表没有滚动效果,我们可以通过添加下面这个计算属性来实现真实的滚动效果
// 偏移量, 用于还原逼真滚效果
const startOffset = computed(
() => scrollTop.value - (scrollTop.value % rowHeight)
);
将偏移量中的scrollTop
替换为startOffset
即可实现平滑滚动。