1.技术栈:vue3
2.原理:通过滚动,展示可视区域数据 ,vue模版里可传入元素宽高,支持多列虚拟滚动。
实现了数据多的情况下页面卡顿的情况,优化性能。
3.虚拟组件手动封装代码:
javascript
<template>
<div class="virtual-list" ref="listContainer" @scroll="onScroll">
<div
class="virtual-list-content"
:style="{
height: totalHeight + 'px',
display: flages ? 'flex' : '',
flexWrap: 'wrap',
alignContent: 'flex-start',
}"
>
<div
v-for="(item, index) in visibleItems"
:key="item.id"
:style="{
width: widthData + 'px',
height: itemHeight + 'px',
transform: `translateY(${offsetY}px)`,
marginTop: type == 1 ? '' : '10px',
}"
class="virtual-list-item"
>
<slot :item="item" :index="index"></slot>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch, nextTick, onActivated, getCurrentInstance } from "vue";
const props = defineProps({
items: {
type: Array,
required: true,
},
itemHeight: {
type: Number,
default: 30,
},
flagFlex: {
type: Boolean,
default: false,
},
type: {
type: Number,
default: 0,
},
widthData: {
type: String,
default: "190",
},
});
const listContainer = ref(null);
const startIndex = ref(0);
const visibleCount = ref(0);
const offsetY = ref(0);
const savedScrollTop = ref(0);
const resizeObserver = ref(null); // 保存 ResizeObserver 实例
const isMounted = ref(false); // 标记组件是否已挂载
const widthData = computed(() => props.widthData);
// 计算列数的函数
const calculateColumnCount = () => {
const container = listContainer.value;
let columnCount = 1;
if (container) {
columnCount = Math.floor(container.clientWidth / parseInt(props.widthData, 10));
columnCount = Math.max(columnCount, 1);
}
return columnCount;
};
const totalHeight = computed(() => {
const columnCount = calculateColumnCount();
const itemsPerColumn = Math.ceil(props.items.length / columnCount);
return itemsPerColumn * props.itemHeight;
});
const flages = computed(() => props.flagFlex);
const types = computed(() => props.type);
const calculateVisibleCount = () => {
const container = listContainer.value;
if (container) {
const containerHeight = container.clientHeight;
const columnCount = calculateColumnCount();
visibleCount.value = Math.ceil(containerHeight / props.itemHeight) * columnCount + columnCount;
}
};
const visibleItems = computed(() => {
const endIndex = startIndex.value + visibleCount.value;
return props.items.slice(startIndex.value, endIndex);
});
const onScroll = () => {
const container = listContainer.value;
if (container) {
const scrollTop = container.scrollTop;
const columnCount = calculateColumnCount();
startIndex.value = Math.floor(scrollTop / props.itemHeight) * columnCount;
offsetY.value = scrollTop - (scrollTop % props.itemHeight);
savedScrollTop.value = scrollTop;
}
};
// 观察容器大小变化的函数
const observeContainerSize = () => {
const container = listContainer.value;
if (container) {
const observer = new ResizeObserver(() => {
calculateVisibleCount();
// 触发 totalHeight 的重新计算
// totalHeight.value;
});
observer.observe(container);
return observer;
}
return null;
};
onMounted(() => {
// 检查组件实例是否存在
const instance = getCurrentInstance();
if (!instance) {
console.warn('VirtualList: No active component instance in onMounted');
return;
}
isMounted.value = true;
// 直接执行初始化,Vue 会确保在 DOM 挂载后执行
try {
if (listContainer.value) {
calculateVisibleCount();
// 监听 resize 事件
window.addEventListener("resize", calculateVisibleCount);
// 监听容器自身大小变化
resizeObserver.value = observeContainerSize();
} else {
// 如果容器还未挂载,等待下一个 tick
nextTick(() => {
// 再次检查组件实例和挂载状态
const currentInstance = getCurrentInstance();
if (currentInstance && isMounted.value && listContainer.value) {
calculateVisibleCount();
window.addEventListener("resize", calculateVisibleCount);
resizeObserver.value = observeContainerSize();
}
});
}
} catch (error) {
// 如果组件已经被卸载,忽略错误
console.warn('VirtualList onMounted error:', error);
}
});
// 在组件卸载时移除监听
onUnmounted(() => {
isMounted.value = false;
window.removeEventListener("resize", calculateVisibleCount);
if (resizeObserver.value && listContainer.value) {
resizeObserver.value.unobserve(listContainer.value);
resizeObserver.value.disconnect();
resizeObserver.value = null;
}
});
onActivated(() => {
const container = listContainer.value;
if (container) {
if (savedScrollTop.value !== 0) {
container.scrollTop = savedScrollTop.value;
}
calculateVisibleCount();
const columnCount = calculateColumnCount();
startIndex.value = Math.floor(savedScrollTop.value / props.itemHeight) * columnCount;
offsetY.value = savedScrollTop.value - (savedScrollTop.value % props.itemHeight);
}
});
// 监听组件显示状态的变化
watch(
() => visibleItems.value,
() => {
const container = listContainer.value;
if (container) {
if (savedScrollTop.value !== 0) {
container.scrollTop = savedScrollTop.value;
}
calculateVisibleCount();
const columnCount = calculateColumnCount();
startIndex.value = Math.floor(savedScrollTop.value / props.itemHeight) * columnCount;
offsetY.value = savedScrollTop.value - (savedScrollTop.value % props.itemHeight);
}
}
);
</script>
<style scoped>
.virtual-list {
overflow-y: auto;
width: 100%;
height: 100%;
position: relative;
background-color: rgb(40, 44, 53);
}
.virtual-list::-webkit-scrollbar {
width: 5px;
}
.virtual-list::-webkit-scrollbar-thumb {
background-color: #2ecc71;
border-radius: 2px;
}
.virtual-list-content {
position: relative;
/* justify-content: space-around; */
}
.virtual-list-item {
box-sizing: border-box;
}
</style>
4.在模版中使用:VirtualList的template标签里的自己根据情况弄自己项目的:ItemComponent需要传入
javascript
<VirtualList :items="checkedCities" :itemHeight="50" :itemComponent="ItemComponent" :flagFlex="true" :type="3" :widthData="'200'">
<template #default="{ item, index }">
<div class="otherxijieevery" v-if="findData(item)">
<div style="display: flex">
<img :src="findData(item).avatar || userInfoStore.imgMAx" alt="" />
<div class="otherxijieevery_every_two0">
<p>
{{ findData(item).nickname }}
</p>
<p>群人数 {{ findData(item).total_member.length }}</p>
</div>
<div
style="
font-size: 11px;
position: absolute;
top: 10px;
right: 5px;
width: 15px;
height: 15px;
border-radius: 50px;
background-color: rgb(40, 44, 53);
border: 1px solid #cdd0d692;
text-align: center;
line-height: 18px;
color: white;
"
@click="clearOne(item)"
>
<el-icon>
<Minus />
</el-icon>
</div>
<div style="font-size: 11px; color: #e6a23c; position: absolute; bottom: 0; right: 5px">
{{ findData(item).is_manager == 1 ? "@群主" : "" }}
</div>
</div>
</div>
</template>
</VirtualList>
//参数ItemComponent
const ItemComponent = {
template: '<div class="custom-item"><slot></slot></div>',
};