前言
在现代Web开发中,当需要渲染大量数据列表时,传统的DOM渲染方式会导致严重的性能问题。虚拟列表(Virtual List)技术通过只渲染可视区域内的元素,大大提升了大数据量列表的渲染性能。
本文将详细介绍三种Vue虚拟列表的实现方案:
-
手写原理实现 - 深入理解虚拟列表核心原理
-
VueUse库实现 - 利用组合式API的强大功能
-
TanStack Virtual - 专业的虚拟化解决方案
技术背景
什么是虚拟列表?
虚拟列表是一种优化长列表渲染性能的技术,其核心思想是:
-
只渲染用户当前可见的列表项
-
通过滚动事件动态更新可见区域
-
使用占位元素维持正确的滚动条高度
为什么需要虚拟列表?
当列表数据量达到数千甚至数万条时,传统渲染方式会遇到:
-
DOM节点过多:导致页面卡顿
-
内存占用高:大量DOM元素占用内存
-
初始渲染慢:首次加载时间过长
-
滚动不流畅:滚动时出现明显延迟
项目环境配置
依赖安装
# 创建Vue项目
npm create vue@latest virtual-list-demo
cd virtual-list-demo
# 安装核心依赖
npm install vue@^3.5.22
# 安装HTTP请求库
npm install axios@^1.12.2
# 安装VueUse(方案二需要)
npm install @vueuse/core@^13.9.0
# 安装TanStack Virtual(方案三需要)
npm install @tanstack/vue-virtual@^3.13.12
# 安装开发依赖
npm install -D @vitejs/plugin-vue@^6.0.1 vite@^7.1.7
package.json 配置
javascript
{
"name": "virtual-list",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/vue-virtual": "^3.13.12",
"@vueuse/core": "^13.9.0",
"axios": "^1.12.2",
"vue": "^3.5.22"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"vite": "^7.1.7"
}
}
方案一:手写原理实现
核心原理
手写实现虚拟列表需要理解以下核心概念:
-
可视区域计算:根据容器高度和项目高度计算可显示的项目数量
-
滚动监听:监听滚动事件,动态计算起始索引
-
位置偏移 :使用
transform: translateY()
定位可视区域 -
占位元素:维持正确的滚动条总高度
完整代码实现
javascript
<script setup>
import { ref, computed, nextTick } from 'vue'
import axios from 'axios'
const LIST_DATA = ref([])
const getData = async () => {
const {data} = await axios.get('/api/mock/68e0c49dbf906d0623008434/api/v1/test#!method=get')
LIST_DATA.value = data.data
}
// 虚拟列表配置
const listHeight = ref(60) // 每个列表项的高度
const showListCount = ref(10) // 可视区域显示的项目数量
const containerHeight = computed(() => showListCount.value * listHeight.value) // 容器高度
// 索引和偏移
const startIndex = ref(0)
const endIndex = computed(() => Math.min(startIndex.value + showListCount.value, LIST_DATA.value.length))
const offsetY = ref(0)
// 显示的数据
const showData = computed(() => {
return LIST_DATA.value.slice(startIndex.value, endIndex.value)
})
// 总高度(用于撑开滚动条)
const totalHeight = computed(() => LIST_DATA.value.length * listHeight.value)
// 滚动事件处理
const handleScroll = (event) => {
const scrollTop = event.target.scrollTop
// 计算当前应该显示的起始索引
startIndex.value = Math.floor(scrollTop / listHeight.value)
// 计算偏移量,用于定位可视区域
offsetY.value = startIndex.value * listHeight.value
}
</script>
<template>
<div class="virtual-list-container">
<h1>手写虚拟列表</h1>
<button @click="getData" class="load-btn">获取数据</button>
<div class="list-info">
<span>总数据量: {{ LIST_DATA.length }}</span>
<span>当前显示: {{ startIndex + 1 }} - {{ endIndex }}</span>
</div>
<!-- 虚拟列表容器 -->
<div
class="virtual-list-wrapper"
:style="{ height: containerHeight + 'px' }"
@scroll="handleScroll"
>
<!-- 占位元素,用于撑开滚动条 -->
<div
class="virtual-list-phantom"
:style="{ height: totalHeight + 'px' }"
></div>
<!-- 可视区域 -->
<div
class="virtual-list-content"
:style="{ transform: `translateY(${offsetY}px)` }"
>
<div
v-for="item in showData"
:key="item.id"
class="list-item"
:style="{ height: listHeight + 'px' }"
>
<div class="item-content">
<span class="item-title">{{ item.title }}</span>
<span class="item-id">ID: {{ item.id }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.virtual-list-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.virtual-list-wrapper {
position: relative;
overflow: auto;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.virtual-list-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.virtual-list-content {
position: absolute;
left: 0;
right: 0;
top: 0;
}
.list-item {
display: flex;
align-items: center;
padding: 0 16px;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s;
}
.list-item:hover {
background-color: #f5f5f5;
}
</style>
API 接口说明
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
listHeight |
number |
60 | 每个列表项的固定高度(px) |
showListCount |
number |
10 | 可视区域显示的项目数量 |
startIndex |
number |
0 | 当前显示的起始索引 |
endIndex |
number |
- | 当前显示的结束索引(计算属性) |
offsetY |
number |
0 | 可视区域的垂直偏移量 |
核心方法
-
handleScroll(event)
: 处理滚动事件,更新显示区域 -
getData()
: 异步获取列表数据
方案二:VueUse 实现
技术优势
VueUse提供了强大的组合式API,让虚拟列表实现更加优雅:
-
useScroll: 响应式滚动监听,支持节流
-
useElementSize: 自动监听元素尺寸变化
-
useAsyncState: 优雅的异步状态管理
-
useThrottleFn: 内置节流函数
完整代码实现
javascript
<script setup>
import { ref, computed } from 'vue'
import { useScroll, useElementSize, useThrottleFn, useAsyncState } from '@vueuse/core'
import axios from 'axios'
// 数据获取
const { state: LIST_DATA, isLoading, execute: getData } = useAsyncState(
async () => {
const { data } = await axios.get('/api/mock/68e0c49dbf906d0623008434/api/v1/test#!method=get')
return data.data
},
[], // 初始值
{ immediate: false } // 不立即执行
)
// 虚拟列表配置
const listHeight = ref(60) // 每个列表项的高度
const showListCount = ref(10) // 可视区域显示的项目数量
const containerHeight = computed(() => showListCount.value * listHeight.value) // 容器高度
// DOM 引用
const scrollContainer = ref()
const listContent = ref()
// 使用 VueUse 的滚动监听
const { y: scrollTop } = useScroll(scrollContainer, {
throttle: 16, // 60fps
idle: 100
})
// 使用 VueUse 的元素尺寸监听
const { height: actualContainerHeight } = useElementSize(scrollContainer)
// 索引和偏移计算
const startIndex = computed(() => Math.floor(scrollTop.value / listHeight.value))
const endIndex = computed(() => Math.min(startIndex.value + showListCount.value + 2, LIST_DATA.value.length)) // +2 缓冲
const offsetY = computed(() => startIndex.value * listHeight.value)
// 显示的数据
const showData = computed(() => {
if (!LIST_DATA.value.length) return []
return LIST_DATA.value.slice(startIndex.value, endIndex.value)
})
// 总高度(用于撑开滚动条)
const totalHeight = computed(() => LIST_DATA.value.length * listHeight.value)
// 虚拟列表状态信息
const virtualListInfo = computed(() => ({
total: LIST_DATA.value.length,
visible: showData.value.length,
startIndex: startIndex.value,
endIndex: endIndex.value,
scrollTop: scrollTop.value,
progress: LIST_DATA.value.length > 0 ? ((scrollTop.value / (totalHeight.value - actualContainerHeight.value)) * 100).toFixed(1) : 0
}))
// 滚动到指定位置的方法
const scrollToIndex = (index) => {
if (scrollContainer.value) {
scrollContainer.value.scrollTop = index * listHeight.value
}
}
// 滚动到顶部/底部
const scrollToTop = () => scrollToIndex(0)
const scrollToBottom = () => scrollToIndex(LIST_DATA.value.length - 1)
</script>
<template>
<div class="virtual-list-container">
<h1>VueUse 虚拟列表</h1>
<!-- 控制面板 -->
<div class="control-panel">
<button @click="getData" class="load-btn" :disabled="isLoading">
{{ isLoading ? '加载中...' : '获取数据' }}
</button>
<div class="info">
<span>总数据量: {{ virtualListInfo.total }}</span>
<span>当前显示: {{ virtualListInfo.startIndex + 1 }} - {{ virtualListInfo.endIndex }}</span>
</div>
</div>
<!-- 虚拟列表容器 -->
<div
ref="scrollContainer"
class="virtual-list-wrapper"
:style="{ height: containerHeight + 'px' }"
>
<!-- 占位元素,用于撑开滚动条 -->
<div
class="virtual-list-phantom"
:style="{ height: totalHeight + 'px' }"
></div>
<!-- 可视区域 -->
<div
ref="listContent"
class="virtual-list-content"
:style="{ transform: `translateY(${offsetY}px)` }"
>
<div
v-for="(item, index) in showData"
:key="item.id"
class="list-item"
:style="{ height: listHeight + 'px' }"
>
<div class="item-content">
<span class="item-title">{{ item.title }}</span>
<span class="item-id">ID: {{ item.id }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
VueUse API 详解
useScroll 配置选项
javascript
const { y: scrollTop } = useScroll(scrollContainer, {
throttle: 16, // 节流延迟(毫秒)
idle: 100, // 空闲检测时间
offset: { // 滚动偏移
top: 0,
bottom: 0,
left: 0,
right: 0
}
})
useAsyncState 配置选项
javascript
const { state, isLoading, error, execute } = useAsyncState(
promiseFunction, // 异步函数
initialState, // 初始状态
{
immediate: false, // 是否立即执行
resetOnExecute: true, // 执行时是否重置状态
shallow: true, // 是否使用浅层响应式
throwError: false // 是否抛出错误
}
)
扩展功能方法
方法名 | 参数 | 返回值 | 说明 |
---|---|---|---|
scrollToIndex(index) |
number |
void |
滚动到指定索引位置 |
scrollToTop() |
- | void |
滚动到列表顶部 |
scrollToBottom() |
- | void |
滚动到列表底部 |
getData() |
- | Promise |
获取列表数据 |
方案三:TanStack Virtual
技术特点
TanStack Virtual 是专业的虚拟化库,具有以下优势:
-
高性能: 专门为虚拟化场景优化
-
灵活配置: 支持动态高度、水平滚动等
-
TypeScript支持: 完整的类型定义
-
跨框架: 支持React、Vue、Solid等多个框架
完整代码实现
javascript
<script setup>
import { ref, computed } from 'vue'
import { useVirtualizer } from '@tanstack/vue-virtual'
import axios from 'axios'
// 数据
const LIST_DATA = ref([])
const isLoading = ref(false)
const getData = async () => {
isLoading.value = true
try {
const { data } = await axios.get('/api/mock/68e0c49dbf906d0623008434/api/v1/test#!method=get')
LIST_DATA.value = data.data
} catch (error) {
console.error('获取数据失败:', error)
} finally {
isLoading.value = false
}
}
// 容器引用
const parentRef = ref()
// 使用 TanStack Virtual - 修复响应式问题
const virtualizer = useVirtualizer(
computed(() => ({
count: LIST_DATA.value.length,
getScrollElement: () => parentRef.value,
estimateSize: () => 60, // 每项高度
overscan: 5, // 缓冲项数
}))
)
</script>
<template>
<div class="container">
<h1>TanStack Virtual 虚拟列表</h1>
<div class="control-panel">
<button @click="getData" :disabled="isLoading">
{{ isLoading ? '加载中...' : '获取数据' }}
</button>
<div class="info">
<span>总数据量: {{ LIST_DATA.length }}</span>
<span v-if="LIST_DATA.length > 0">虚拟项目数: {{ virtualizer.getVirtualItems().length }}</span>
</div>
</div>
<!-- 虚拟列表容器 -->
<div
v-if="LIST_DATA.length > 0"
ref="parentRef"
class="list-container"
:style="{ height: '600px', overflow: 'auto' }"
>
<div
:style="{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}"
>
<div
v-for="item in virtualizer.getVirtualItems()"
:key="item.key"
:style="{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${item.size}px`,
transform: `translateY(${item.start}px)`,
}"
class="list-item"
>
<div class="item-content">
<span>{{ LIST_DATA[item.index]?.title || `项目 ${item.index + 1}` }}</span>
<span class="item-id">ID: {{ LIST_DATA[item.index]?.id || item.index + 1 }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
TanStack Virtual API 详解
useVirtualizer 配置选项
javascript
const virtualizer = useVirtualizer({
count: 1000, // 总项目数量
getScrollElement: () => parentRef.value, // 滚动容器元素
estimateSize: () => 50, // 估算每项高度
overscan: 5, // 缓冲区项目数量
horizontal: false, // 是否水平滚动
paddingStart: 0, // 起始填充
paddingEnd: 0, // 结束填充
scrollMargin: 0, // 滚动边距
gap: 0, // 项目间距
indexAttribute: 'data-index', // 索引属性名
initialOffset: 0, // 初始偏移量
getItemKey: (index) => index, // 获取项目key的函数
rangeExtractor: defaultRangeExtractor, // 范围提取器
measureElement: undefined, // 测量元素函数
scrollToFn: elementScrollToFn, // 滚动函数
})
核心方法和属性
方法/属性 | 类型 | 说明 |
---|---|---|
getVirtualItems() |
VirtualItem[] |
获取当前虚拟项目列表 |
getTotalSize() |
number |
获取总的虚拟尺寸 |
scrollToIndex(index, options?) |
void |
滚动到指定索引 |
scrollToOffset(offset, options?) |
void |
滚动到指定偏移量 |
measure() |
void |
重新测量所有项目 |
VirtualItem 对象结构
javascript
interface VirtualItem {
key: Key // 项目唯一标识
index: number // 项目索引
start: number // 项目起始位置
end: number // 项目结束位置
size: number // 项目尺寸
}
三种方案性能对比
性能指标对比
指标 | 手写实现 | VueUse实现 | TanStack Virtual |
---|---|---|---|
初始化性能 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
滚动流畅度 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
内存占用 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
代码复杂度 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
可扩展性 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
学习成本 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
适用场景分析
手写实现
适用场景:
-
学习虚拟列表原理
-
项目对第三方依赖有严格限制
-
需要完全自定义的简单场景
优势:
-
无额外依赖
-
代码可控性强
-
学习价值高
劣势:
-
功能相对简单
-
需要自己处理边界情况
-
维护成本高
VueUse实现
适用场景:
-
Vue3项目中已使用VueUse
-
需要响应式的滚动监听
-
中等复杂度的虚拟列表需求
优势:
-
组合式API风格
-
丰富的响应式工具
-
与Vue3生态完美集成
劣势:
-
需要额外学习VueUse API
-
相比专业库功能有限
TanStack Virtual
适用场景:
-
高性能要求的大数据量列表
-
需要复杂虚拟化功能(动态高度、水平滚动等)
-
专业的数据展示应用
优势:
-
专业的虚拟化解决方案
-
性能优异
-
功能完整
-
TypeScript支持完善
劣势:
-
学习成本相对较高
-
包体积相对较大
实际项目集成指南
1. 选择合适的方案
javascript
// 根据项目需求选择方案
const chooseVirtualListSolution = (requirements) => {
if (requirements.learningPurpose) {
return '手写实现'
}
if (requirements.dataSize < 1000 && requirements.complexity === 'simple') {
return 'VueUse实现'
}
if (requirements.dataSize > 5000 || requirements.complexity === 'complex') {
return 'TanStack Virtual'
}
return 'VueUse实现' // 默认推荐
}
2. 性能优化建议
javascript
// 通用优化策略
const optimizationTips = {
// 1. 使用节流函数
throttleScroll: true,
throttleDelay: 16, // 60fps
// 2. 设置合适的缓冲区
overscan: 5,
// 3. 避免在滚动时进行复杂计算
avoidHeavyComputation: true,
// 4. 使用固定高度提升性能
useFixedHeight: true,
// 5. 合理设置可视区域大小
visibleItemCount: 10-15
}
3. 常见问题解决
问题1:滚动时出现白屏
javascript
// 解决方案:增加缓冲区
const config = {
overscan: 5, // 增加缓冲项目数量
throttle: 16 // 降低节流延迟
}
问题2:动态高度支持
javascript
// TanStack Virtual 支持动态高度
const virtualizer = useVirtualizer({
count: data.length,
getScrollElement: () => parentRef.value,
estimateSize: (index) => {
// 根据内容估算高度
return data[index]?.content?.length > 100 ? 120 : 60
},
measureElement: (element) => {
// 实际测量元素高度
return element.getBoundingClientRect().height
}
})
总结
本文详细介绍了三种Vue虚拟列表实现方案,每种方案都有其适用场景:
-
手写实现:适合学习原理和简单场景
-
VueUse实现:适合中等复杂度的Vue3项目
-
TanStack Virtual:适合高性能要求的专业应用
选择合适的方案需要考虑:
-
项目规模和性能要求
-
团队技术栈和学习成本
-
功能复杂度和扩展需求
希望本文能帮助您在实际项目中选择和实现最适合的虚拟列表方案!