在Vue3中实现虚拟列表的方法示例
作者:程序员张张
引言
在开发过程中,我们有时会遇到数据量较大的情况,这会导致大量数据同时加载到页面,从而生成过多的 DOM 元素。这种情况不仅会导致页面卡顿,甚至可能导致浏览器直接崩溃。给用户体验带来极大的负面影响。为了解决这一问题,我们可以采用虚拟列表技术,通过只渲染可视区域内的元素,显著提升页面的性能和用户体验。
现在网上有许多现成的虚拟列表第三方插件库,我们可以直接使用这些库。然而,这边我打算自己动手去实现虚拟列表功能。在之前的 Vue 2 项目中,我已经实现过类似的功能,这次我打算利用 Vue 3 来重新实现,并将其封装成一个公共组件。
虚拟列表的基本原理
虚拟列表通过只渲染当前可视区域内的列表项,从而提高长列表加载到页面的性能。
- 设置子数据项高度:确定子数据项的具体高度。以确定当前区域内需要渲染的列表项。
- 计算可视区域高度:确定当前可视区域内可渲染多少条子数据项,计算起始下标、结束下标。避免渲染整个列表。
- 渲染可视区域:保持渲染的DOM节点数量始终在一个较小的范围内,通过动态调整渲染内容的位置,保持列表高度完整且滚动条能正常滚动。
- 滚动监听:监听容器的滚动事件,实时获取滚动位置,通过滚动位置实时更新可视区域范围,动态渲染对应列表项。
- 设置缓冲列表项:在可视区域的上下各增加一定数量的缓冲列表项,提前加载即将进入可视区域的列表项,避免滚动时出现空白以及卡顿的情况。
好的!接下来,我们将通过代码一步步实现上述功能,完整呈现虚拟列表的核心逻辑和效果。
代码实现
1、设置子数据项的高度
子数据项的高度是固定值,所以这里就定义了个变量。(注:子数据项的高度与css中的高度保持一致)代码如下:
<script lang="ts" setup> // 子数据项高度 const itemHeight = 40 </script>
2、计算可视区域高度、起始下标、结束下标
因为下面会通过滚动条的高度去计算详细的值。所以这里我们的起始下标和结束下标使用计算属性去定义。代码如下:
<script lang="ts" setup> // 可视区域的高度 const viewHeight = ref(0) // ref虚拟列表容器dom const virtualContainer = ref<HTMLElement | null>(null) // 在dom加载完成后,通过ref去获取可视区域的高度 onMounted(() => { nextTick(() => { viewHeight.value = virtualContainer.value?.clientHeight ?? 0 }) }) // 虚拟列表真实展示数据:起始下标 const start = computed(() => { return 0 }) // 虚拟列表真实展示数据:结束下标 const end = computed(() => { return viewHeight.value / itemHeight }) </script>
3、渲染可视区域
paddingAttr
的目的是保持列表的高度完整,并确保滚动条能够正常滚动。由于实际渲染的 DOM 元素较少,可能导致滚动条位置异常,因此需要通过设置 padding
来撑起容器的高度。此外,也可以使用 transform
和 position
来实现这一效果。代码如下:
<div ref="virtualContainer" @scroll="onScroll" class="virtual-container"> <div class="virtual-list"> <div class="virtual-item" v-for="item in virtualData" :key="item.id"> <div class="item">{{ item.title }}</div> </div> </div> </div> <script lang="ts" setup> // 大数据数组 const dataList = reactive<any[]>([]) for (let i = 0; i < 100000; i++) { dataList.push({ id: i, title: `标题${i}` }) } // 计算虚拟列表的padding(保持列表高度完整且滚动条能正常滚动) const paddingAttr = computed(() => { const paddingTop = start.value * itemHeight const paddingBottom = (dataList.length - over.value) * itemHeight return `${paddingTop}px 0 ${paddingBottom}px` }) // 虚拟列表真实展示数据 const virtualData = computed(() => { return dataList.slice(start.value, over.value) }) </script> <style lang="scss" scoped> .virtual-container { overflow-y: auto; height: 100%; .virtual-list { padding: v-bind(paddingAttr); .virtual-item { text-align: center; height: 30px; line-height: 30px; background: #84bbfc; margin-bottom: 10px; } } } </style>
4、滚动监听
上面我们初步的定义了起始下标、结束下标,但那并不满足我们的需求,这边我们通过监听滚动事件,获取到滚动条位置,通过滚动条位置去重新计算起始下标、结束下标。代码如下:
<script lang="ts" setup> // 滚动条距离顶部距离 const scrollTop = ref(0) // 虚拟列表真实展示数据:起始下标 const start = computed(() => { const s = Math.floor(scrollTop.value / itemHeight) return Math.max(0, s) }) // 虚拟列表真实展示数据:结束下标 const over = computed(() => { const o = Math.floor((scrollTop.value + viewHeight.value + 1) / itemHeight) return Math.min(dataList.length, o) }) // 监听滚动条距离顶部距离,实时更新 const onScroll = () => { scrollTop.value = virtualContainer.value?.scrollTop ?? 0 } </script>
5、设置缓冲列表项
这里给起始下标和结束下标,各自加减一个固定值,我这边设置的值是5,这边可以设置成其他值,但不能太大会影响性能。太小的话滚动会卡顿和出现白屏问题。代码如下:
<script lang="ts" setup> // 虚拟列表真实展示数据:起始下标 const start = computed(() => { const s = Math.floor(scrollTop.value / itemHeight - 5) return Math.max(0, s) }) // 虚拟列表真实展示数据:结束下标 const over = computed(() => { const o = Math.floor((scrollTop.value + viewHeight.value + 1) / itemHeight + 5) return Math.min(dataList.length, o) }) </script>
好了,下面是虚拟列表的完整的代码:
<template> <div ref="virtualContainer" @scroll="onScroll" class="virtual-container"> <div class="virtual-list"> <div class="virtual-item" v-for="item in virtualData" :key="item.id"> <div class="item">{{ item.title }}</div> </div> </div> </div> </template> <script lang="ts" setup> import { computed, nextTick, onMounted, ref, reactive } from 'vue' /** * 虚拟列表的每一项的高度 */ const itemHeight = 40 const dataList = reactive<any[]>([]) for (let i = 0; i < 100000; i++) { dataList.push({ id: i, title: `标题${i}` }) } /** * 滚动条距离顶部距离 */ const scrollTop = ref(0) /** * ref虚拟列表容器dom */ const virtualContainer = ref<HTMLElement | null>(null) /** * 可视区域的高度 */ const viewHeight = ref(0) // 在dom加载完成后,获取可视区域的高度 onMounted(() => { nextTick(() => { viewHeight.value = virtualContainer.value?.clientHeight ?? 0 }) }) /** * 虚拟列表真实展示数据:起始下标 */ const start = computed(() => { const s = Math.floor(scrollTop.value / itemHeight) return Math.max(0, s) }) /** * 虚拟列表真实展示数据:结束下标 */ const over = computed(() => { const o = Math.floor((scrollTop.value + viewHeight.value + 1) / itemHeight) return Math.min(dataList.length, o) }) /** * 计算虚拟列表的padding(保持列表高度完整且滚动条能正常滚动) */ const paddingAttr = computed(() => { const paddingTop = start.value * itemHeight const paddingBottom = (dataList.length - over.value) * itemHeight return `${paddingTop}px 0 ${paddingBottom}px` }) /** * 虚拟列表真实展示数据 */ const virtualData = computed(() => { return dataList.slice(start.value, over.value) }) /** * 监听滚动条距离顶部距离,实时更新 */ const onScroll = () => { scrollTop.value = virtualContainer.value?.scrollTop ?? 0 } </script> <style lang="scss" scoped> .virtual-container { overflow-y: auto; height: 100%; .virtual-list { padding: v-bind(paddingAttr); .virtual-item { text-align: center; height: 30px; line-height: 30px; background: #84bbfc; margin-bottom: 10px; } } } ::-webkit-scrollbar { width: 12px; height: 12px; background: #ffffff; border-radius: 6px; } ::-webkit-scrollbar-thumb { background: #00a6ff; border-radius: 6px; } </style>
示例:
组件封装
上面我们完成了虚拟列表的功能实现,但是呢,在现实的开发中我们会遇到不止一个长列表的需求,每一个都这么写,会有很多冗余的代码,而且很麻烦。所以在这里我们将其封装成一个公共的组件。以简化我们日常开发的代码量和时间成本。
这边封装组件的逻辑和上面基本一致,我就不多赘述了,直接上代码:
<template> <div ref="virtualContainer" @scroll="onScroll" class="virtual-container"> <div class="virtual-list"> <slot v-if="slotDefault" name="default" :dataList="virtualData"></slot> <template v-else> <div class="virtual-item" v-for="item in virtualData" :key="item[keyField]" :style="{ height: itemHeight + 'px', lineHeight: itemHeight + 'px' }" > <slot name="item" :item="item"></slot> </div> </template> </div> </div> </template> <script lang="ts" setup name="VirtualList"> import { withDefaults, defineProps, computed, nextTick, onMounted, ref, useSlots } from 'vue' /** * 虚拟列表defineProps接口(类型约束) * @param dataList 数据列表 * @param keyField 每一项的唯一标识key * @param itemHeight 每一项的高度 * @param containerHeight 容器高度 */ interface virtualProps { dataList: any[] keyField?: string itemHeight?: number containerHeight?: string } /** * 父组件传入的值 * withDefaults 为props设置默认值 */ const { dataList, keyField, itemHeight, containerHeight } = withDefaults(defineProps<virtualProps>(), { keyField: 'id', itemHeight: 40, containerHeight: '100%' }) /** * 滚动条距离顶部距离 */ const scrollTop = ref(0) /** * ref虚拟列表容器dom */ const virtualContainer = ref<HTMLElement | null>(null) /** * 可视区域的高度 */ const viewHeight = ref(0) onMounted(() => { nextTick(() => { viewHeight.value = virtualContainer.value?.clientHeight ?? 0 }) }) /** * 虚拟列表真实展示数据:起始下标 */ const start = computed(() => { const s = Math.floor(scrollTop.value / itemHeight - 5) return Math.max(0, s) }) /** * 虚拟列表真实展示数据:结束下标 */ const over = computed(() => { const o = Math.floor((scrollTop.value + viewHeight.value + 1) / itemHeight + 5) return Math.min(dataList.length, o) }) /** * 计算虚拟列表的padding(保持列表高度完整且滚动条能正常滚动) */ const paddingAttr = computed(() => { const paddingTop = start.value * itemHeight const paddingBottom = (dataList.length - over.value) * itemHeight return `${paddingTop}px 0 ${paddingBottom}px` }) /** * 虚拟列表真实展示数据 */ const virtualData = computed(() => { return dataList.slice(start.value, over.value) }) /** * 监听滚动条距离顶部距离,实时更新 */ const onScroll = () => { scrollTop.value = virtualContainer.value?.scrollTop ?? 0 } /** * 获取默认插槽 */ const slotDefault = useSlots().default </script> <style lang="scss" scoped> .virtual-container { overflow-y: auto; height: v-bind(containerHeight); .virtual-list { padding: v-bind(paddingAttr); .virtual-item { text-align: center; border: 1px solid orangered; } } } ::-webkit-scrollbar { width: 12px; height: 12px; background: #ffffff; border-radius: 6px; } ::-webkit-scrollbar-thumb { background: #00a6ff; border-radius: 6px; } </style>
这边我们的代码里面定义了两个插槽,default
插槽是为了满足element-ui
中的下拉框长列表问题。
代码如下:
<template> <div style="height: 100%"> <div style="width: 240px; height: 100%"> <el-select multiple v-model="activeName" @visible-change="visibleChange"> <VirtualList v-if="visibleState" :data-list="data" :item-height="34" container-height="194px"> <template #default="{ dataList }"> <el-option v-for="i in dataList" :label="i.title" :value="i.id" :key="i.id" /> </template> </VirtualList> </el-select> </div> </div> </template> <script lang="ts" setup> import VirtualList from '@/components/VirtualList/index.vue' import { reactive, ref } from 'vue' const data = reactive<any[]>([]) for (let i = 0; i < 100000; i++) { data.push({ id: i, title: `标题${i}` }) } const activeName = ref('') const visibleState = ref(false) const visibleChange = (val: boolean) => { visibleState.value = val } </script>
文章小尾巴
以上就是在Vue3中实现虚拟列表的方法示例的详细内容,更多关于Vue3虚拟列表的资料请关注脚本之家其它相关文章!