在前端开发中,当我们需要展示上万甚至几十万条数据时,直接渲染整个列表会导致页面卡顿、滚动不流畅,严重影响用户体验。虚拟列表(Virtual List)技术通过只渲染可视区域内的列表项,大幅提升长列表性能,成为解决这类问题的最佳方案。
本文将介绍两种实现虚拟列表的方式:使用成熟的第三方库 vue-virtual-scroller
快速集成,以及手动实现一个简易虚拟列表深入理解其原理。
一、使用 vue-virtual-scroller 快速实现
vue-virtual-scroller
是 Vue 生态中最流行的虚拟列表库之一,支持固定高度、动态高度列表,甚至网格布局,兼容性好且易用性高。
1. 安装依赖
首先通过 npm 或 yarn 安装库:
javascript
# npm
npm install vue-virtual-scroller --save
# yarn
yarn add vue-virtual-scroller
同时需要引入配套样式(全局引入或局部引入均可
javascript
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
2. 全局注册(可选)
在 main.js
中全局注册组件,方便全项目使用:
javascript
import { createApp } from 'vue'
import App from './App.vue'
import { VueVirtualScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
const app = createApp(App)
app.use(VueVirtualScroller) // 全局注册
app.mount('#app')
3. 基础使用示例
创建一个包含 10 万条数据的虚拟列表:
javascript
<template>
<div class="virtual-list-demo">
<h3>vue-virtual-scroller 实现虚拟列表</h3>
<RecycleScroller
class="scroller"
:items="largeList"
:item-size="60" <!-- 单个列表项高度 -->
key-field="id" <!-- 唯一标识字段 -->
>
<!-- 列表项模板 -->
<template #default="{ item }">
<div class="list-item">
<span class="item-id">{{ item.id }}</span>
<span class="item-content">{{ item.content }}</span>
</div>
</template>
</RecycleScroller>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 局部引入(如果未全局注册)
import { RecycleScroller } from 'vue-virtual-scroller'
// 生成 10 万条模拟数据
const largeList = ref(
Array.from({ length: 100000 }, (_, i) => ({
id: i + 1,
content: `这是第 ${i + 1} 条列表项,使用 vue-virtual-scroller 渲染`
}))
)
</script>
<style scoped>
.scroller {
height: 500px; /* 必须设置容器高度 */
width: 800px;
border: 1px solid #e5e7eb;
border-radius: 4px;
}
.list-item {
height: 60px; /* 与 item-size 保持一致 */
padding: 0 20px;
display: flex;
align-items: center;
border-bottom: 1px solid #f3f4f6;
}
.item-id {
width: 60px;
color: #6b7280;
font-weight: 500;
}
.item-content {
color: #111827;
}
</style>
4. 核心参数说明
:items
:绑定需要渲染的数据源数组:item-size
:单个列表项的高度(固定高度时使用)key-field
:列表项的唯一标识字段(用于优化重渲染)v-slot:default="{ item }"
:通过插槽获取当前渲染的列表项数据
5. 动态高度支持
如果列表项高度不固定,可以使用 DynamicScroller
组件:
javascript
<DynamicScroller
class="scroller"
:items="dynamicList"
:min-item-size="50" <!-- 最小高度估计值 -->
>
<template #default="{ item, index, active }">
<DynamicScrollerItem
:item="item"
:active="active"
:index="index"
>
<!-- 动态高度的列表项内容 -->
<div class="dynamic-item" :style="{ height: `${item.height}px` }">
{{ item.content }}
</div>
</DynamicScrollerItem>
</template>
</DynamicScroller>
二、手动实现简易虚拟列表
为了深入理解虚拟列表的工作原理,我们可以手动实现一个简化版。核心逻辑是:根据滚动位置计算可视区域内的列表项范围,只渲染这部分内容。
1. 实现思路
- 固定容器高度:设置列表容器的固定高度并开启滚动
- 计算总高度:通过占位元素模拟整个列表的总高度(让滚动条正常显示)
- 监听滚动事件:获取滚动距离,计算可视区域内的列表项索引范围
- 定位可见项:通过定位(transform)将可见项放置到正确位置
javascript
<template>
<div class="manual-virtual-list">
<h3>手动实现虚拟列表</h3>
<!-- 滚动容器 -->
<div
class="list-container"
@scroll="handleScroll"
ref="listContainer"
>
<!-- 占位元素:模拟总高度 -->
<div
class="list-placeholder"
:style="{ height: totalHeight + 'px' }"
></div>
<!-- 可见项容器:通过定位放置可见项 -->
<div class="visible-items" :style="{ transform: `translateY(${offsetY}px)` }">
<div
class="list-item"
v-for="item in visibleItems"
:key="item.id"
>
<span class="item-id">{{ item.id }}</span>
<span class="item-content">{{ item.content }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
// 配置参数
const ITEM_HEIGHT = 60 // 单个项高度
const CONTAINER_HEIGHT = 500 // 容器高度
const BUFFER = 5 // 额外渲染的缓冲项数量(避免滚动时白屏)
// 生成 10 万条模拟数据
const largeList = ref(
Array.from({ length: 100000 }, (_, i) => ({
id: i + 1,
content: `这是第 ${i + 1} 条列表项,手动实现虚拟列表`
}))
)
// 滚动相关状态
const scrollTop = ref(0) // 滚动距离顶部的距离
const listContainer = ref(null)
// 计算总高度(所有项的总高度)
const totalHeight = computed(() => largeList.value.length * ITEM_HEIGHT)
// 计算可见项的起始索引
const startIndex = computed(() => {
// 减去缓冲项,提前渲染
return Math.max(0, Math.floor(scrollTop.value / ITEM_HEIGHT) - BUFFER)
})
// 计算可见项的结束索引
const endIndex = computed(() => {
// 可见项数量 = 容器高度 / 项高度 + 缓冲项
const visibleCount = Math.ceil(CONTAINER_HEIGHT / ITEM_HEIGHT) + BUFFER * 2
return Math.min(largeList.value.length, startIndex.value + visibleCount)
})
// 可见项数据
const visibleItems = computed(() => {
return largeList.value.slice(startIndex.value, endIndex.value)
})
// 可见项容器的偏移量(让可见项对齐正确位置)
const offsetY = computed(() => {
return startIndex.value * ITEM_HEIGHT
})
// 处理滚动事件
const handleScroll = (e) => {
scrollTop.value = e.target.scrollTop
}
onMounted(() => {
// 初始化容器高度
if (listContainer.value) {
listContainer.value.style.height = `${CONTAINER_HEIGHT}px`
}
})
</script>
<style scoped>
.list-container {
position: relative;
width: 800px;
overflow-y: auto;
border: 1px solid #e5e7eb;
border-radius: 4px;
}
.list-placeholder {
/* 占位元素不占布局空间,但需要高度 */
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.visible-items {
/* 可见项容器需要脱离文档流,通过 transform 定位 */
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.list-item {
height: 60px; /* 与配置的 ITEM_HEIGHT 一致 */
padding: 0 20px;
display: flex;
align-items: center;
border-bottom: 1px solid #f3f4f6;
}
/* 与前面相同的列表项样式 */
.item-id {
width: 60px;
color: #6b7280;
font-weight: 500;
}
.item-content {
color: #111827;
}
</style>
3. 核心逻辑解析
-
占位元素(list-placeholder)
这是手动实现的关键之一,通过设置
height: totalHeight
模拟整个列表的高度,让浏览器生成正确的滚动条。如果没有它,容器内只有少量可见项,滚动条会无法正常工作。 -
可见项计算
startIndex
:根据滚动距离scrollTop
计算当前需要渲染的起始索引(减去缓冲项提前渲染)endIndex
:根据容器高度和项高度计算需要渲染的结束索引(加上缓冲项避免滚动白屏)visibleItems
:通过数组切片获取需要渲染的可见项数据
-
定位可见项
通过
transform: translateY(${offsetY}px)
调整可见项容器的位置,让可见项始终对齐滚动后的可视区域。offsetY
等于起始索引乘以项高度,确保可见项的位置与完整列表一致。三、两种方式对比与选择
实现方式 优点 缺点 适用场景 vue-virtual-scroller 功能完善、支持动态高度、优化好 增加依赖体积、有学习成本 生产环境、复杂场景(动态高度、网格布局) 手动实现 无依赖、轻量、理解原理 功能简单、不支持动态高度 简单场景、学习研究、高度固定的列表