前言
在日常开发中,我们经常会遇到需要展示大量数据的场景,比如展示几千条甚至上万条数据列表。如果直接将所有数据渲染到页面中,会导致严重的性能问题,页面会变得卡顿甚至崩溃。虚拟列表(Virtual List)正是为了解决这一问题而诞生的技术。
本文将介绍虚拟列表的核心原理,并基于 Vue3 手写一个完整的虚拟列表组件。
一、什么是虚拟列表
虚拟列表是一种按需渲染的技术,它只渲染当前可视区域内的列表项,而不是一次性渲染所有数据。想象一下,你有一本1000页的书,但你在任何时候只能看到其中的一页。虚拟列表的工作方式就和翻书一样------只展示当前"看得到"的内容。
虚拟列表的核心优势
| 优势 | 说明 |
|---|---|
| 性能提升 | 只渲染可见区域的数据,DOM 节点数量大幅减少 |
| 内存优化 | 避免创建大量 DOM 节点,降低内存占用 |
| 流畅体验 | 滚动更加流畅,不会因为数据量大而卡顿 |
| 快速首屏 | 首屏渲染时间大大缩短 |
二、虚拟列表的实现原理
虚拟列表的核心思想其实很简单,可以用以下几个步骤来概括:
- 计算可视区域:根据滚动位置和容器高度,计算出当前可见的数据范围
- 按需渲染:只渲染计算出的可见数据,而不是全部数据
- 占位模拟:通过 padding 或 transform 模拟完整的滚动高度,让滚动条看起来正常
下面这张图清晰地展示了虚拟列表的工作原理:
┌─────────────────────────────┐
│ 已渲染区域(可见) │
│ ┌─────────────────────┐ │
│ │ Item 3 │ │
│ │ Item 4 │ │
│ │ Item 5 │ │ ← 当前可见区域
│ │ Item 6 │ │
│ │ Item 7 │ │
│ └─────────────────────┘ │
├─────────────────────────────┤
│ 未渲染区域(不可见) │
│ ... Item 8-100 │
└─────────────────────────────┘
三、代码实现
子组件:MyList.vue
vue
<template>
<div style="overflow-y:auto ;border:3px solid blue;position: relative;" :style="{ height:props.size * 10 + 'px'}" @scroll="handSrool">
<ul :style="{ height:(props.size+1) * props.testData.length + 'px', transform: `translateY(${offsetY}px)` }" >
<li v-for="(item, index) in list" class="list-item" :style="{ height:props.size + 'px', position: 'absolute', top: (index * props.size) + 'px', width: '100%' }">
{{ item }}
</li>
</ul>
</div>
</template>
<script setup>
import { onMounted, ref, computed } from 'vue';
// 接受父节点参数
const props = defineProps({
testData: Array,
size: {
type: Number,
default: '6'
}
})
const startIndex = ref(0)
const endIndex = computed(() => startIndex.value + 10)
// 计算偏移量,让列表项跟随滚动
const offsetY = computed(() => startIndex.value * props.size)
const list = computed(() => props.testData.slice(startIndex.value, endIndex.value))
// 监听滚动
const handSrool = (e) => {
// 获取滚动距离
const scrollTop = e.target.scrollTop
// 根据滚动距离计算起始索引
startIndex.value = Math.floor(scrollTop / props.size)
}
onMounted(() => {
console.log(props.size)
console.log(endIndex.value)
})
</script>
<style>
.list-item {
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid rgb(146, 144, 144);
}
</style>
父组件:使用示例
vue
<script setup>
import MyList from './components/MyList.vue';
const data = []
// 模拟数据
for (let i = 0; i < 100; i++) {
data.push({
id: i,
name: `name${i}`,
age: i,
sex: i % 2 === 0 ? '男' : '女',
address: `address${i}`,
date: new Date(),
})
}
</script>
<template>
<div class="table_continue">
<MyList :testData="data" :size="60"/>
</div>
</template>
<style scoped>
.table_continue {
padding: 20px;
}
</style>
四、核心实现解析
1. 计算可见区域的起始索引
javascript
const handSrool = (e) => {
const scrollTop = e.target.scrollTop
startIndex.value = Math.floor(scrollTop / props.size)
}
这是虚拟列表最核心的逻辑。当用户滚动时,我们通过 scrollTop 获取滚动距离,然后除以单个列表项的高度,就能计算出当前应该从第几条数据开始渲染。
为什么要这样做?
假设每个列表项高度是 60px,滚动位置是 300px,那么 300 / 60 = 5,说明用户已经滚过了5个列表项,所以我们应该从第5条数据开始渲染。
2. 计算可见数据
javascript
const list = computed(() => props.testData.slice(startIndex.value, endIndex.value))
使用 slice 方法截取数组中的一部分,只渲染从 startIndex 开始的 10 条数据。这样无论原数组有多大,DOM 中最多只有 10 个列表项元素。
3. 使用 transform 模拟滚动
javascript
const offsetY = computed(() => startIndex.value * props.size)
这是实现无缝滚动的关键。虽然我们只渲染了 10 条数据,但通过 transform: translateY() 将整个列表向下偏移,偏移量等于被"跳过"的数据的总高度。这样用户看起来就像在滚动整个列表一样。
4. 占位容器的高度
javascript
:style="{ height:(props.size+1) * props.testData.length + 'px' }"
外层容器的高度等于单个列表项高度乘以总数据条数,这就是让滚动条看起来正常的"秘密"。浏览器会根据这个高度生成相应长度的滚动条,用户拖动滚动条时就会触发 scroll 事件。
五、性能优化建议
虽然上面的实现已经能够正常工作,但在生产环境中,你可能还需要考虑以下优化点:
1. 预渲染更多数据
可以在可见区域的前后各多渲染几条数据,避免快速滚动时出现空白:
javascript
const BUFFER = 3
const endIndex = computed(() => startIndex.value + 10 + BUFFER)
const list = computed(() => {
const start = Math.max(0, startIndex.value - BUFFER)
return props.testData.slice(start, endIndex.value)
})
2. 使用防抖处理滚动事件
如果数据量非常大,可以使用防抖来减少计算频率:
javascript
import { debounce } from 'lodash-es'
const handSrool = debounce((e) => {
const scrollTop = e.target.scrollTop
startIndex.value = Math.floor(scrollTop / props.size)
}, 16)
3. 处理列表项高度不一致的情况
在实际应用中,列表项的高度可能是动态的。这时可以使用测量行高的方案,或者将所有列表项设为固定高度。
4. 使用 CSS will-change 优化动画
css
.list-item {
will-change: transform;
}
这告诉浏览器该元素会发生变化,可以提前进行优化。
六、原理图解
让我们通过一张图来理解整个虚拟列表的工作流程:
用户滚动 → 获取 scrollTop → 计算 startIndex → slice 取数据 → translateY 偏移
│ │
↓ ↓
┌─────────┐ ┌─────────────┐ ┌──────────┐ ┌────────────┐
│ 滚动事件 │ → │ scrollTop │ → │ 300/60=5 │ → │ 取第5-15条 │
│ 触发 │ │ = 300px │ │ start=5 │ │ translateY │
└─────────┘ └─────────────┘ └──────────┘ │ = 300px │
└────────────┘
七、总结
虚拟列表是前端性能优化中非常重要的技术之一。它的核心思想可以用一句话概括:只渲染可见的内容,用占位符模拟完整的滚动高度。
通过本文的实现,我们可以看到一个基础的虚拟列表组件并不复杂,主要涉及到以下几个关键点:
- 监听滚动事件:获取用户的滚动位置
- 计算可见索引:根据滚动位置计算应该显示的数据范围
- 按需渲染:只渲染计算范围内的数据
- 模拟滚动:通过 transform 或 padding 模拟完整的滚动高度
掌握了这些核心原理后,你就可以在此基础上进行各种定制化开发,比如支持动态高度的列表项、添加加载更多功能、实现分组虚拟列表等。
希望本文对你理解虚拟列表有所帮助!