Skip to content

虚拟滚动列表组件

元素高度相等的元素的虚拟滚动组件

开发背景

  • 测试那边要测 5000-10000 条的数据,且一些列表不像表格的有分页器而是滚动展示,数据量过大会导致DOM过多浏览器卡顿😢。
  • 同时这个也是面试中人家最爱问的问题之一,后端一下传给你10w条数据你该怎么展示😂。

开发思路

  • 首先进行调研实现的基本方式,基本大差不差。基于一个窗口盒子和一个撑开滚动高度的盒子组成基本的容器,剩下的里面的元素根据滚动的高度来计算一个要展示的元素索引范围进行绝对定位即可。
  • 经过上面的分析,需要虚拟滚动的元素是外部控制的,而这些外部的元素是只需进行对容器的绝对定位即可。因此组件可以定位为一个维护虚拟滚动数据和样式的容器即可,再通过 vue 的作用域插槽实现对外部元素的传参即可。

实现代码

  • lb-scroll 组件
vue
<template>
	<!-- 虚拟滚动组件(只适合高度相等的元素) -->
	<div class="lb-scroll-box" ref="lbScrollBoxRef" @scroll="handleScroll">
		<div class="lb-scroll-box_content" :style="contentHeight">
			<slot :renderList="renderList"></slot>
		</div>
	</div>
</template>

<script>
export default {
	props: {
		// 虚拟滚动的元素个数
		itemCount: {
			type: Number,
			default: 100,
		},
		// 虚拟滚动的元素的高度
		itemSize: {
			type: Number,
			default: 100,
		},
		// 上缓冲区的元素个数
		startCacheCount: {
			type: Number,
			default: 2,
		},
		// 下缓冲区的元素个数
		endCacheCount: {
			type: Number,
			default: 2,
		},
	},
	data() {
		return {
			scrollOffset: 0, //记录滚动条滚动的距离
			height: 0, //撑开滚动条的高度
		};
	},
	computed: {
		contentHeight() {
			return `height:${this.itemCount * this.itemSize}px`;
		},
		renderList() {
			// 可视区起始索引
			const startIndex = Math.floor(this.scrollOffset / this.itemSize);
			// 上缓冲区起始索引
			const finialStartIndex = Math.max(0, startIndex - this.startCacheCount);
			// 可视区能展示的元素的最大个数
			const numVisible = Math.ceil(this.height / this.itemSize);
			// 下缓冲区结束索引
			const endIndex = Math.min(this.itemCount - 1, startIndex + numVisible + this.endCacheCount);
			const items = [];
			// 根据上面计算的索引值,不断添加元素给container
			for (let i = finialStartIndex; i <= endIndex; i++) {
				const itemStyle = {
					style: {
						position: 'absolute',
						top: this.itemSize * i + 'px', // 计算每个元素在container中的top值
						height: this.itemSize + 'px',
						width: '100%',
						zIndex: 0,
					},
					index: i,
				};
				items.push(itemStyle);
			}
			return items;
		},
	},
	methods: {
		handleScroll(e) {
			this.scrollOffset = e.target.scrollTop;
		},
	},
	mounted() {
		this.$nextTick(() => {
			this.height = this.$refs.lbScrollBoxRef.offsetHeight;
		});
	},
};
</script>

<style lang="less" scoped>
.lb-scroll-box {
	height: 100%;
	overflow-y: auto;
	position: relative;
}
</style>
  • 调用的业务页面代码
vue
<template>
	<lb-scroll v-if="dataList.length" :itemSize="25" :itemCount="dataList.length + 1">
		<template v-slot="{ renderList }">
			<div v-for="(item, index) in renderList" :key="index" :class="['provider-item', item.index === clickIndex ? 'is-active' : '']" :style="item.style" @click="handleClick(item.index)">
				<template v-if="dataList[item.index]">
					<span class="provider-name">{{ dataList[item.index].name }}</span>
					<el-button type="text" @click="detail(dataList[item.index])"> 详情 </el-button>
				</template>
				<template v-else>
					<div class="next-page-btn">
						<el-button type="text" :disabled="searchForm.currentPage === totalPage" :loading="nextLoading" @click.stop="loadNextList">
							{{ searchForm.currentPage === totalPage ? '加载完毕' : '加载更多' }}
						</el-button>
					</div>
				</template>
			</div>
		</template>
	</lb-scroll>
</template>