前言
虚拟列表(Virtual List)是什么?这是一种前端性能优化的技术,主要是应用于渲染超长列表的场景。其核心原理是:只渲染可视区域内的列表项,而非全部数据,从而大幅度地减少了DOM节点的数量,提升页面性能。
最近刚好了解到虚拟列表的相关内容,虚拟列表主要有定高和不定高两种情景,其实现方法还挺有意思的。本文实现的是一个定高的虚拟列表,也就是说每一项的高度是固定的,因此我们可以通过计算得知整个列表的高度。
示意图
定高的虚拟列表的示意图如下所示。
主要分为以下几个部分:
-
外部容器:固定高度(即可视区域的高度),超出隐藏;监听页面的 scroll 滚动事件。
-
占位元素区域:设置的是所有内容的高度,用于撑开滚动区域,其高度会远远大于外部容器的高度以实现滚动的效果。(其实不加占位元素也是可以的,把 totalHeight 设置到 .scroll-content 元素)
-
实际列表区域:实际渲染的列表内容是很少的,通过 transform 属性模拟滚动效果。
接下来详细分析其实现原理。
实现原理
首先,页面框架是这样设置的:容器是 .virtual-scroll,占位元素是 .scroll-phantom,实际渲染的列表是 .scroll-content,其中,每一项是<div class="item"></div>。而 totalHeight 是占位元素的总高度,是动态设置的。
<template>
<div class="virtual-scroll" ref="container" @scroll="handleScroll">
<!-- 占位元素,撑开滚动区域 -->
<div class="scroll-phantom" :style="{ height: totalHeight + 'px' }"></div>
<!-- 实际渲染的列表 -->
<div class="scroll-content" :style="{ transform: `translateY(${offset}px)` }">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
</div>
</template>
<style scoped>
.virtual-scroll {
height: 100%;
overflow: auto;
position: relative;
}
.scroll-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.scroll-content {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.item {
display: flex;
align-items: center;
padding: 0 16px;
border-bottom: 1px solid #eee;
box-sizing: border-box;
}
</style>
接着,我们不妨将列表每一项的高度固定为 60 px,总数据量 totalCount 定为 1000,那么 totalHeight 就是 60,000 px。
我们生成全部的模拟数据 allData,而可视区域的数据 visibleData 获取的是 allData 的 [startIndex, endIndex] 范围的数据。
那么,我们会有以下几个疑问:
1、我们需要考虑的是如何确定 startIndex 和 endIndex 的值?
将 startIndex 初始化为 0,在每次滚动页面时,动态更新。而 endIndex 的值为 startIndex + visibleCount。
在每次滚动时,startIndex 的值更新为 Math.floor(scrollTop / itemHeight),即滚动距离/每一项的高度。
2、我们怎么模拟滚动效果?
对于实际渲染的列表 .scroll-content ,通过设置样式{ transform: `translateY(${offset}px)` },offset 就是页面向上滑动的距离,在每次滚动页面时,动态更新即可。
3、怎么获取 offset 的值?
代码中,offset.value = scrollTop - (scrollTop % itemHeight),即列表的Y轴偏移量是页面的滚动距离 - (滚动距离 % 单项高度),这样是为了保证偏移量是单项高度的整数倍,使得内容在滚动时保持对齐,避免出现半截项的情况。
至此,核心原理就是这样。
理解了上面的几个问题,也就不难掌握定高的虚拟列表的实现方法了。
完整代码
完整的示例代码如下:
<template>
<div class="virtual-scroll" ref="container" @scroll="handleScroll">
<!-- 占位元素,撑开滚动区域 -->
<div class="scroll-phantom" :style="{ height: totalHeight + 'px' }"></div>
<!-- 实际渲染的列表 -->
<div class="scroll-content" :style="{ transform: `translateY(${offset}px)` }">
<div
v-for="item in visibleData"
:key="item.id"
class="item"
:style="{ height: itemHeight + 'px' }"
>
{{ item.content }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
const itemHeight = 60 // 每项高度
const totalCount = 1000 // 总数据量
// 生成模拟数据
const allData = Array.from({ length: totalCount }, (_, i) => ({
id: i,
content: `Item ${i + 1}`
}))
const container = ref(null)
const containerHeight = ref(0)
const startIndex = ref(0)
const offset = ref(0)
const visibleCount = ref(0)
// 计算属性
const totalHeight = computed(() => allData.length * itemHeight)
const endIndex = computed(() => startIndex.value + visibleCount.value)
const visibleData = computed(() => allData.slice(startIndex.value, endIndex.value))
// 初始化
onMounted(() => {
containerHeight.value = container.value.clientHeight
visibleCount.value = Math.ceil(containerHeight.value / itemHeight) + 1
})
// 滚动处理
function handleScroll() {
const scrollTop = container.value.scrollTop
startIndex.value = Math.floor(scrollTop / itemHeight)
offset.value = scrollTop - (scrollTop % itemHeight)
}
</script>
<style scoped>
.virtual-scroll {
height: 100%;
overflow: auto;
position: relative;
}
.scroll-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.scroll-content {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.item {
display: flex;
align-items: center;
padding: 0 16px;
border-bottom: 1px solid #eee;
box-sizing: border-box;
}
</style>
后记
简而言之,定高的虚拟列表是一种前端性能优化的技术,可以应用于渲染超长列表的场景。
开卷有益!