无限极服务
146.43MB · 2025-10-24
盒子的三高总是分不清楚,虚拟列表总是听说却没实际写过。详细学习下做个记录 盒子
clientHeight 元素可视区高度 content+padding = 102
offsetHeight 元素clientHeight+boreder + 滚动条 = 150
scrollHeight 元素实际高 = 102
scrollTop 滚动上方超出可视区height
场景:需要展示数千、数万甚至更多条数据(如聊天记录、日志、表格行、商品列表等)。
问题:直接渲染全部数据会导致:
虚拟列表优势:
template 模版
<div @scroll="handleScroll" ref="listContainer">
<!-- 占位元素维持滚动条 -->
<div class="virtual-list-container" :style="{ height: totalHeight + 'px' }">
<!-- 渲染可视区域内的列表项 -->
<div
v-for="item in visibleItems"
:key="item.index"
:style="{ transform: `translateY(${item.offset}px)` }
>
<!-- 实际列表项内容 --> {{item.data}}
</div>
</div>
</div>
基础逻辑
<script setup>
import { ref, computed, reactive, onMounted } from 'vue';
// 列表数据源
const props = defineProps({
items: {
type: Array,
required: true
},
itemHeight: {
type: Number,
required: true
}
});
// 列表容器引用
const listContainer = ref(null);
// 可视区域项目
const visibleItems = reactive([]
// 滚动位置
const scrollTop = ref(0);
// 总高度(用于占位元素)
const totalHeight = computed(() => {
return props.items.length * props.itemHeight;
});
// 可视区域高度
const visibleHeight = ref(0);
// 可视区域项目数量
const visibleCount = computed(() => {
return Math.ceil(visibleHeight.value / props.itemHeight)
});
// 计算当前可视项目
const visibleItems = computed(() => {
const start = Math.floor(scrollTop.value / props.itemHeight);
const end = start + visibleCount.value;
return Array.from({ length: end - start }, (_, i) => {
const index = start + i;
const data = props.items[index] || null;
return {
index,
offset: index * props.itemHeight,
data
};
});
});
// 滚动处理
function handleScroll() {
scrollTop.value = listContainer.value.scrollTop;
}
// 初始化可视区域高度
onMounted(() => {
visibleHeight.value = listContainer.value.clientHeight;
});
</script>
window.addEventListener('resize')
getBoundingClientRect()或offsetHeight等获取高度// 列表容器引用
const listContainer = ref(null);
window.addEventListener('resize', () => {
visibleHeight.value = listContainer.value.clientHeight;
// 需额外处理节流/防抖
});
resizeObsever
// 列表容器引用
const listContainer = ref(null);
const resizeObserver = new ResizeObserver(() => {
visibleHeight.value = listContainer.value.clientHeight
});
onUnmounted(() => {
window.removeEventListener('resize', ()=>{...});
resizeObserver.disconnect();
});
性能问题 滚动卡顿,页面卡死
-方案: requestAnimationFrame()帧渲染,在浏览器每帧的空余时间进行,避免频繁的重排
function handleScroll() {
requestAnimationFrame(() => {
scrollTop.value = listContainer.value.scrollTop;
});
}
function updateVisibleHeight() {
requestAnimationFrame(() => {
visibleHeight.value = listContainer.value.clientHeight;
});
}
快速滚动出现空白或闪烁
const bufferSize = ref(5)
// 计算当前可视项目
const visibleItems = computed(() => {
const start = Math.floor(scrollTop.value / props.itemHeight) + bufferSize.value*2;
const end = start + visibleCount.value - bufferSize;
...
});
getBoundingClientRect()获取元素位置信息<div
class="list-item"
v-for="item in visibleItems"
:key="item.index"
:data-index="item.index"
:style="{ transform: `translateY(${item.offset}px)` }"
@load="measureItemHeight(item.index)"
>
const itemHeights = ref([]); // 动态高度缓存
function measureItemHeight(index) {
const item = document.querySelector(`.list-item[data-index="${index}"]`);
if (item) {
itemHeights.value[index] = item.getBoundingClientRect().height;
}
}
function getOffset(index) {
return itemHeights.value.slice(0, index).reduce((pre, height) => pre + height, 0);
}
props.items)动态更新时,滚动位置可能错位。
watch(() => props.items, (newItems) => {
if (newItems.length !== props.items.length) {
const ratio = newItems.length / props.items.length;
scrollTop.value *= ratio;
}
}, { deep: true });