当前位置: 首页 > news >正文

Vue 虚拟列表实现方案详解:三种方法的完整对比与实践

 前言

在现代Web开发中,当需要渲染大量数据列表时,传统的DOM渲染方式会导致严重的性能问题。虚拟列表(Virtual List)技术通过只渲染可视区域内的元素,大大提升了大数据量列表的渲染性能。

本文将详细介绍三种Vue虚拟列表的实现方案:

  1. 手写原理实现 - 深入理解虚拟列表核心原理

  2. VueUse库实现 - 利用组合式API的强大功能

  3. 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 配置

{"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()定位可视区域

  • 占位元素:维持正确的滚动条总高度

完整代码实现

<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 接口说明

属性类型默认值说明
listHeightnumber60每个列表项的固定高度(px)
showListCountnumber10可视区域显示的项目数量
startIndexnumber0当前显示的起始索引
endIndexnumber-当前显示的结束索引(计算属性)
offsetYnumber0可视区域的垂直偏移量

核心方法

  • handleScroll(event): 处理滚动事件,更新显示区域

  • getData(): 异步获取列表数据

 方案二:VueUse 实现

技术优势

VueUse提供了强大的组合式API,让虚拟列表实现更加优雅:

  • useScroll: 响应式滚动监听,支持节流

  • useElementSize: 自动监听元素尺寸变化

  • useAsyncState: 优雅的异步状态管理

  • useThrottleFn: 内置节流函数

完整代码实现

<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, // 60fpsidle: 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 配置选项
const { y: scrollTop } = useScroll(scrollContainer, {throttle: 16,    // 节流延迟(毫秒)idle: 100,       // 空闲检测时间offset: {        // 滚动偏移top: 0,bottom: 0,left: 0,right: 0}
})
useAsyncState 配置选项
const { state, isLoading, error, execute } = useAsyncState(promiseFunction,  // 异步函数initialState,     // 初始状态{immediate: false,     // 是否立即执行resetOnExecute: true, // 执行时是否重置状态shallow: true,        // 是否使用浅层响应式throwError: false     // 是否抛出错误}
)

扩展功能方法

方法名参数返回值说明
scrollToIndex(index)numbervoid滚动到指定索引位置
scrollToTop()-void滚动到列表顶部
scrollToBottom()-void滚动到列表底部
getData()-Promise获取列表数据

方案三:TanStack Virtual

技术特点

TanStack Virtual 是专业的虚拟化库,具有以下优势:

  • 高性能: 专门为虚拟化场景优化

  • 灵活配置: 支持动态高度、水平滚动等

  • TypeScript支持: 完整的类型定义

  • 跨框架: 支持React、Vue、Solid等多个框架

完整代码实现

<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 = truetry {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>
​<!-- 虚拟列表容器 --><divv-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',}"><divv-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 配置选项
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 对象结构
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. 选择合适的方案

// 根据项目需求选择方案
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. 性能优化建议

// 通用优化策略
const optimizationTips = {// 1. 使用节流函数throttleScroll: true,throttleDelay: 16, // 60fps// 2. 设置合适的缓冲区overscan: 5,// 3. 避免在滚动时进行复杂计算avoidHeavyComputation: true,// 4. 使用固定高度提升性能useFixedHeight: true,// 5. 合理设置可视区域大小visibleItemCount: 10-15
}

3. 常见问题解决

问题1:滚动时出现白屏
// 解决方案:增加缓冲区
const config = {overscan: 5, // 增加缓冲项目数量throttle: 16 // 降低节流延迟
}
问题2:动态高度支持
// 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虚拟列表实现方案,每种方案都有其适用场景:

  1. 手写实现:适合学习原理和简单场景

  2. VueUse实现:适合中等复杂度的Vue3项目

  3. TanStack Virtual:适合高性能要求的专业应用

选择合适的方案需要考虑:

  • 项目规模和性能要求

  • 团队技术栈和学习成本

  • 功能复杂度和扩展需求

希望本文能帮助您在实际项目中选择和实现最适合的虚拟列表方案!

参考资源

  • Vue3 官方文档

  • VueUse 官方文档

  • TanStack Virtual 官方文档

  • 虚拟列表原理详解

http://www.dtcms.com/a/446164.html

相关文章:

  • Oracle OCP认证考试题目详解082系列第48题
  • 第一章:单例模式 - 武林中的孤高剑客
  • sql题目基础50题
  • 哪些网站做的最好网站建设功能报
  • 第十三章:眼观六路,耳听八方——Observer的观察艺术
  • Kubernetes集群安全机制
  • 建站行业的发展趋势网站建设网络
  • AI大事记9:从 AlexNet 到 ChatGPT——深度学习的十年跃迁(下)
  • 网站收录了但是搜索不到全网霸屏推广系统
  • 张量分解 | CP / Tucker / BTD
  • 网站推广及建设ppt河北网站建设企业
  • 【数据结构】二叉搜索树的递归与非递归实现
  • 九亭镇村镇建设办官方网站1688接代加工订单
  • GJOI 9.27/10.3 题解
  • Python实例入门
  • 多线程核心知识点与高并发应用指南
  • 南宁网站建设nnxun政策变了2022二建有必要考吗
  • ASP3605电源芯片关键指标测试说明
  • Spring——事件机制
  • UMI企业智脑4.0与5.0的先进性之争,从“AI工具”到“孪生数字人”,赋能每个员工
  • 城乡建设查询网站网站维护包括
  • 从国标到自动化:VSTO实现身份证智能解析(待测)
  • 租凭境外服务器做违规网站wordpress 幻灯片主题
  • 网站开发团队简介如何写链接网站制作
  • php 8.4.5 更新日志
  • MongoDB 连接时的**认证参数配置错误**
  • 大兴安岭做网站葫芦岛建设工程信息网站
  • 商标设计网站提供哪些服务建筑书店
  • 除 OpenAI/GPT-4o 等主流头部产品外,值得关注的 AI 及 Agent 产品有哪些?
  • Vue 3 —— M / 接口文档