在Vue3中实现虚拟列表的方法示例

 更新时间:2025年01月15日 09:21:05   作者:程序员张张  
文章主要介绍在 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 来撑起容器的高度。此外,也可以使用 transformposition 来实现这一效果。代码如下:

<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虚拟列表的资料请关注脚本之家其它相关文章!

相关文章

  • vue中数据绑定值(字符串拼接)的几种实现方法

    vue中数据绑定值(字符串拼接)的几种实现方法

    这篇文章主要介绍了vue中数据绑定值(字符串拼接)的几种实现方法,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-07-07
  • 如何解决vue在ios微信

    如何解决vue在ios微信"复制链接"功能问题

    这篇文章主要介绍了如何解决vue在ios微信"复制链接"功能问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-03-03
  • cdn模式下vue的基本用法详解

    cdn模式下vue的基本用法详解

    这篇文章主要介绍了cdn模式下vue的基本用法,本文通过图文并茂的形式给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友参考下吧
    2018-10-10
  • Element Tooltip 文字提示的使用示例

    Element Tooltip 文字提示的使用示例

    这篇文章主要介绍了Element Tooltip 文字提示的使用示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-07-07
  • Vue中map()的用法案例

    Vue中map()的用法案例

    map()函数定义在JS的array中,它返回一个新的数组,下面这篇文章主要给大家介绍了关于Vue中map()的用法案例,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
    2022-07-07
  • vue项目中常见问题及解决方案(推荐)

    vue项目中常见问题及解决方案(推荐)

    这篇文章主要介绍了vue项目中常见问题及解决方案,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-10-10
  • Vue中watch、computed、updated三者的区别及用法

    Vue中watch、computed、updated三者的区别及用法

    这篇文章主要介绍了Vue中watch、computed、updated三者的区别及用法说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-07-07
  • vue和iview结合动态生成表单实例

    vue和iview结合动态生成表单实例

    这篇文章主要介绍了vue和iview结合动态生成表单实例,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-10-10
  • Vue设计器form-create-designer配置表单默认值示例详解

    Vue设计器form-create-designer配置表单默认值示例详解

    这篇文章主要介绍了如何使用开源项目form-create-designer来灵活调整表单的默认值,通过config.formOptions,您可以自定义表单的全局布局,文章提供了一个详细的例子,展示了如何使用form-create-designer的配置选项来调整表单的布局和外观,感兴趣的朋友一起看看吧
    2024-11-11
  • Vue使用exceljs导出excel文件的详细教程

    Vue使用exceljs导出excel文件的详细教程

    这篇文章主要为大家详细介绍了Vue如何使用exceljs导出excel文件的详细教程,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2025-03-03

最新评论