盒子的三高总是分不清楚,虚拟列表总是听说却没实际写过。详细学习下做个记录 盒子
clientHeight 元素可视区高度 content+padding = 102
offsetHeight 元素clientHeight+boreder + 滚动条 = 150
scrollHeight 元素实际高 = 102
scrollTop 滚动上方超出可视区height
虚拟列表
-
场景:需要展示数千、数万甚至更多条数据(如聊天记录、日志、表格行、商品列表等)。
-
问题:直接渲染全部数据会导致:
- DOM节点过多,内存占用高。
- 布局计算(Reflow)和重绘(Repaint)耗时过长,滚动卡顿。
-
虚拟列表优势:
- 仅渲染可视区域(如屏幕内)的几十条数据,大幅减少DOM节点。
- 滚动时动态计算并更新可见内容,保持流畅体验。
代码实现
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>
基础逻辑
- 获取可视区高
- 计算可视区域展示数量
- 选择数据展示区间
- 计算每组列表scrollTop值
- 处理滚动事件
<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- 监听指定DOM元素的尺寸变化
- 可以直接获取元素高度
- 仅在元素变化是触发回调
- 浏览器会自动合并回调,减少重排/重绘
// 列表容器引用
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;
});
}
-
快速滚动出现空白或闪烁
- 方案:使用缓存区域 ,可以避免重读触发scroll事件
- 方法:每次加载数据时,头尾多加载几条数据
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 监听items
watch(() => props.items, (newItems) => {
if (newItems.length !== props.items.length) {
const ratio = newItems.length / props.items.length;
scrollTop.value *= ratio;
}
}, { deep: true });