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

vue3中开发一个不定高的虚拟滚动组件

开发虚拟滚动的不定高组件
开发的过程中我们只要处理一个问题即可。renderList,即渲染的数据列表
我们带着如何获取renderList这个问题去进行逻辑梳理
首先组件内部接收两个值,渲染的数据和每一项的高度

const {list, itemHeight} = defineProps({
  list: { // 渲染的数据
    type: Array,
    default: () => [],
  },
  itemHeight: { // 预估每一项的高度
    type: Number,
    default: 100,
  },
})

我们先去计算renderList(页面可视区域渲染的列表)

const renderList = computed(() => list.slice(startIndex.value, endIndex.value))

想要获取renderList需要知道页面可视区域的第一条数据和最后一条数据的下标,初始化的时候,startIndex 的值为0.,随着滚动更新startIndex,endIndex的值为startIndex+renderCount(可视区域的数量);所以我们的代码如下:

const renderCount = computed(() => Math.ceil(containerHeight.value/itemHeight))
const endIndex = computed(() => startIndex.value + renderCount.value)

其中的containerHeight为可视区域的高度:

containerHeight.value = containerRef.value.clientHeight || 0;

因为以上的renderList是我们根据预估的高度来进行计算的,我们要想得到真实的renderList,需要获取到真实的高度
获取startIndex我们需要根据列表的每项的真实高度来计算startIndex的值,我们定义一个变量来存储每项的下标(index)、top、bottom和height。

const position = ref([]);
function initPosition() {
	position.value = [];
	list.forEach((d, i) => {
		position.value.push({
			index: i,
			height: itemHeight,
			top: i * itemHeight,
			bottom: (i + 1) * itemHeight,
		});
	});
}

每次获取到list数据以后我们初始化position。

watch(() => list, () => {
	initPosition();
},{
	immediate: true
})

此时获取的都是最小高度,我们获取真实高度的时候要等页面上渲染以后才能获取到,所以我们要等页面更新完dom以后进行更新:

<template>
	<div ref="containerRef" class="container" @scroll="handleScroll">
		<div class="container-list" :style="scrollStyle" ref="listRef">
			<div class="container-list-item" v-for="item in renderList" :key="item.index" :itemid="item.index">
				{{ item.index }}{{ item.content  }}
			</div>

		</div>

	</div>
</template>
<script setup>
onUpdated(() => {
	updatePosition();
})
function updatePosition(){
	//获取listRef下的子元素
  const nodes = listRef.value ? listRef.value.children : [];
	if(!nodes?.length) return;
	const data = [...nodes];
	// 遍历所有的子元素更新真实的高度
	data.forEach(el => {
		let index = +el.getAttribute('itemid');
		const realHeight = el.getBoundingClientRect().height;
		// 判断默认的高度和真实的高度之差
		let diffVal = position.value[index].height - realHeight;
		if (diffVal !== 0) {
			for(let i = index; i < position.value.length; i++) {
				position.value[i].height = realHeight;
				position.value[i].top = position.value[i].top - diffVal;
				position.value[i].bottom = position.value[i].bottom - diffVal;
			}
		}
	})
}
</script>

代码中的itemid为完整数据的下标,保存下来更新position的的值的时候会用到。

获取到真实的高度以后我们就能计算startIndex了,如果item.bottom > scrollTop (滚动的高度)&& item.top <= scrollTop则,当前数据为可视区域的第一项,因为position中的bottom的值是递增的,我们只需要找到第一个bottom > scrollTop的值的下标即可,position.value.findIndex(item => item.bottom > scrollTop)。
使用二分法查找进行优化:

function handleScroll(e) {
	const scrollTop = e.target.scrollTop;
	startIndex.value = getStartIndex(scrollTop);
}
// 优化前
function getStartIndex(scrollTop) {
	return position.value.findIndex(item => item.bottom > scrollTop)
}
// 优化后
const getStartIndex = (scrollTop) => {
	let left = 0;
	let right = position.value.length - 1;
	while (left <= right) {
		const mid = Math.floor((left + right) / 2);
		if(position.value[mid].bottom == scrollTop) {
			return mid + 1;
		} else if (position.value[mid].bottom > scrollTop) {
			right = mid - 1;
		} else if (position.value[mid].bottom < scrollTop) {
			left = mid + 1;
		}
	}
	return left;
}

至此我们就获取到了我们需要的renderList,我们只需要给list容易写上样式即可,list的高度为:position的最后一项的bottom-滚动卷上去的高度,其中卷上去的高度为可视区域第一项的top值。

// 卷上去的高度
const scrollTop = computed(()=> startIndex.value > 0 ? position.value[startIndex.value-1].bottom : 0);
// list元素的整体高度
const listHeight = computed(() => position.value[position.value.length - 1].bottom);
const scrollStyle = computed(() => {
	return {
		height:`${listHeight.value - scrollTop.value}px`,
		transform: `translate3d(0, ${scrollTop.value}px, 0)`,
	}
})

完整代码:
父组件:

<template>
	<div class="virtual-scroll">
		<Viru :list="list" :item-size="50"/>
	</div>
</template>
<script setup>
import { getListData } from './data';
import { ref } from 'vue';
import Viru from './virtualUnfixedList.vue';
/**
 * list格式为:
 * [
 *   {
 *      index: 1,
 *      conten: 'xxx'
 *   },
 *   {
 *      index: 2,
 *      content: 'xxx'
 *   }
 * ]
 */
const list = ref(getListData());
</script>
<style lang="scss" scoped>
.virtual-scroll {
	height: 500px; 
	width: 500px;
	border: 1px solid red;
}

</style>

子组件:

<template>
	<div ref="containerRef" class="container" @scroll="handleScroll">
		<div class="container-list" :style="scrollStyle" ref="listRef">
			<div class="container-list-item" v-for="item in renderList" :key="item.index" :itemid="item.index">
				{{ item.index }}{{ item.content  }}
			</div>

		</div>

	</div>
</template>
<script setup>
import { ref, computed, watch, onMounted, onUpdated } from 'vue'

const {list, itemHeight} = defineProps({
  list: { // 渲染的数据
    type: Array,
    default: () => [],
  },
  itemHeight: { // 预估每一项的高度
    type: Number,
    default: 100,
  },
})
const containerRef = ref(null);
const listRef = ref(null);
const startIndex = ref(0);
const containerHeight = ref(0);
const position = ref([]);

const scrollTop = computed(()=> startIndex.value > 0 ? position.value[startIndex.value-1].bottom : 0);
const listHeight = computed(() => position.value[position.value.length - 1].bottom);

const scrollStyle = computed(() => {
	return {
		height:`${listHeight.value - scrollTop.value}px`,
		transform: `translate3d(0, ${scrollTop.value}px, 0)`,
	}
})

const renderCount = computed(() => Math.ceil(containerHeight.value/itemHeight))
const endIndex = computed(() => startIndex.value + renderCount.value)

const renderList = computed(() => {
	return list.slice(startIndex.value, endIndex.value);
})


onMounted(() => {
	containerHeight.value = containerRef.value.clientHeight || 0;
})
onUpdated(() => {
	updatePosition();
})

watch(() => list, () => {
	initPosition();
},{
	immediate: true
})
function initPosition() {
	position.value = [];
	console.log(list);
	list.forEach((d, i) => {
		position.value.push({
			index: i,
			height: itemHeight,
			top: i * itemHeight,
			bottom: (i + 1) * itemHeight,
		});
	});
}
function updatePosition(){
	//获取listRef下的子元素
  const nodes = listRef.value ? listRef.value.children : [];
	if(!nodes?.length) return;
	const data = [...nodes];
	console.log(nodes)
	// 遍历所有的子元素更新真实的高度
	data.forEach(el => {
		let index = +el.getAttribute('itemid');
		const realHeight = el.getBoundingClientRect().height;
		// 判断默认的高度和真实的高度之差
		let diffVal = position.value[index].height - realHeight;
		if (diffVal !== 0) {
			for(let i = index; i < position.value.length; i++) {
				position.value[i].height = realHeight;
				position.value[i].top = position.value[i].top - diffVal;
				position.value[i].bottom = position.value[i].bottom - diffVal;
			}
		}
	})
}
function handleScroll(e) {
	const scrollTop = e.target.scrollTop;
	startIndex.value = getStartIndex(scrollTop);
}

const getStartIndex = (scrollTop) => {
	let left = 0;
	let right = position.value.length - 1;
	while (left <= right) {
		const mid = Math.floor((left + right) / 2);
		if(position.value[mid].bottom == scrollTop) {
			return mid + 1;
		} else if (position.value[mid].bottom > scrollTop) {
			right = mid - 1;
		} else if (position.value[mid].bottom < scrollTop) {
			left = mid + 1;
		}
	}
	return left;
}

</script>
<style scoped lang="scss">
.container {
  width: 100%;
	height: 100%;
  overflow: auto;
  &-list{
    width: 100%;
    &-item{
      width: 100%;
    }
  }
}
</style>

http://www.kler.cn/a/460413.html

相关文章:

  • 【MySQL】--- 内置函数
  • java class类对象 加载时机
  • 慧集通iPaaS集成平台低代码训练-实践篇
  • 机器学习经典算法——线性回归
  • 利用多GPU,推理transformer模型,避免显存溢出
  • Ubuntu 24.04 LTS 解决网络连接问题
  • SpringBoot + Thymeleaf + Bootstrap5 整合 MyBatis-Plus 和 MySQL 实现动态分类标签渲染教程
  • 郑州时空-TMS运输管理系统 GetDataBase 信息泄露漏洞复现
  • 深信服云桌面系统的终端安全准入设置
  • JavaWeb开发(三)Servlet技术-手动、自动创建Servlet
  • Elasticsearch-搜索推荐:Suggest
  • 记一次 .NET某汗液测试机系统 崩溃分析
  • HTML5 开关(Toggle Switch)详细讲解
  • 如何将联系人从 iPhone 转移到华为 [4 种方法]
  • 【SQLi_Labs】Basic Challenges
  • 网络游戏之害
  • 智能工厂的设计软件 应用场景的一个例子:为AI聊天工具添加一个知识系统 之9 重新开始 之2
  • 半导体数据分析: 玩转WM-811K Wafermap 数据集(一) AI 机器学习
  • 机器学习DAY9:聚类(K-means、近邻传播算法、谱聚类、凝聚聚类、兰德指数、调整互信息、V−mearure、轮廓系数)
  • Python爬虫入门实例:Python7个爬虫小案例(附源码)
  • 解锁节日季应用广告变现潜力,提升应用广告收入
  • Flink读写Kafka(DataStream API)
  • springboot523基于Spring Boot的大学校园生活信息平台的设计与实现(论文+源码)_kaic
  • 【数据库系统概念】期末复习笔记
  • Spring MVC (下)小项目实战
  • SD卡恢复数据:快速找回丢失文件!