1. 技术背景
虚拟滚动(Virtual Scrolling)是一种优化长列表渲染性能的技术。当列表数据量很大时,传统方式渲染所有 DOM 元素会导致严重的性能问题。虚拟滚动通过只渲染可视区域内的元素,大幅减少 DOM 节点数量,从而提升渲染性能和用户体验。
2. 固定高度(v1)
2.1 核心实现原理
该组件基于以下核心思想实现:
- 通过一个固定高度的容器作为视口(viewport)
- 使用一个与全部数据等高的占位元素(scroll-bg)产生滚动条
- 动态计算并渲染可见区域内的数据项
- 利用 translate3d 实现高效的列表项定位
2.2 关键代码解析
模板结构
ini
<template>
<div class="viewport" ref="viewport" @scroll="handleScroll">
<div class="scroll-bg" ref="scrollBg"></div>
<div
class="scroll-list"
ref="scrollList"
:style="{ transform: `translate3d(0, ${offset}px,0)` }"
>
<template v-for="item in visibleItems" :key="item.id">
<slot :item="item"></slot>
</template>
</div>
</div>
</template>
Props 定义
ini
const props = defineProps<{
items: any[]; // 列表数据
remain: number; // 显示个数
size: number; // 每个元素的高度
}>();
核心状态管理
用start和end来记录当前显示的屏幕需要显示的数组的起始位置,但是在滚动过程中会存在一个列表元素滚动了一部分的情况,会有空白的显示问题,所以在增加前后预加载的数据,相当于加载3个屏幕的数据;
用offset记录显示列表数据的父元素的偏移量,偏移量为用户滑动过完整的列表个数和列表高度的乘积;
对传入的总数据进行截取,只显示预加载和屏幕正在显示的列表数据,以此来提高渲染性能,提高用户体验;
ini
// 数组的起始值
const start = ref(0);
const end = ref(props.remain);
// 数组渲染dom的偏移量
const offset = ref(0);
// 前面预先加载的个数
const prevCount = computed(() => {
return Math.min(start.value, props.remain);
});
// 后面预先加载的个数
const nextCount = computed(() => {
return Math.min(props.items.length - end.value, props.remain);
});
// 计算当前需要显示的数据
const visibleItems = computed(() => {
const startIndex = start.value - prevCount.value;
const endIndex = end.value + nextCount.value;
return props.items.slice(startIndex, endIndex);
});
滚动事件处理
获取滚动的距离,重新计算屏幕中显示数据的开始位置和结束位置,更新偏移量
ini
const handleScroll = () => {
// 滚动的距离
const scrollTop = viewportRef.value?.scrollTop ?? 0;
// 滚动过去的完整个数
const scrollCount = Math.floor(scrollTop / props.size);
start.value = scrollCount;
end.value = start.value + props.remain;
offset.value = start.value * props.size - prevCount.value * props.size;
};
2.3 性能优化策略
- 只渲染可视区域内的元素,减少 DOM 节点数量
- 使用 translate3d 进行硬件加速,提高动画性能
- 预加载前后缓冲区数据,避免快速滚动时白屏
- 利用 Vue 的响应式系统精确更新数据切片
2.4 使用示例
ruby
<template>
<virtual-scroll-list :size="40" :items="items" :remain="10">
<template v-slot="{ item }">
<constant-item :title="item.title"></constant-item>
</template>
</virtual-scroll-list>
</template>
3. 动态高度(v2)
当列表的内容是不确定的,可变的时候,固定高度不再满足业务需求,需要增加可变的选项来满足可变高度的列表
3.1 核心实现原理
3.1.1 Position Cache(位置缓存)
- 维护每个列表项的位置信息(top, bottom, height)
- 初始时基于默认大小预估位置
- 动态更新实际渲染后的准确位置
3.1.2 Binary Search(二分查找)
- 在可变高度模式下,使用二分查找快速定位可见区域的起始索引
- 相比线性查找大幅提升滚动性能
3.2 算法详解
3.2.1 二分查找算法
用于快速定位当前滚动位置对应的起始项索引:
区别于平常的二分算法,由于可能滚动的位置在一个列表元素的中间位置,所以增加temp变量来记录当前当前最上方显示的元素在数据列表中的索引;
ini
const binarySearch = (scrollTop) => {
let start = 0;
let end = positions.length - 1;
let temp = null;
while (start <= end) {
let mid = (start + end) | 0;
let midBottom = positions[mid].bottom;
if (scrollTop === midBottom) {
return mid + 1;
} else if (scrollTop < midBottom) {
if (temp === null || temp > mid) {
temp = mid;
}
end = mid - 1;
} else {
start = mid + 1;
}
}
return temp || 0;
};
3.2.2 位置更新算法
当元素高度发生变化时,更新位置缓存:
ini
const { height } = el.getBoundingClientRect();
const id = Number(el.getAttribute("vid")) || 0;
const oldHeight = positions.find((p) => p.id === id)?.height ?? 0;
const diffHeight = height - oldHeight;
if (diffHeight !== 0) {
// 高度有变化
const index = positions.findIndex((p) => p.id === id);
positions[index]!.height = oldHeight + diffHeight;
positions[index]!.bottom = positions[index]!.top + height;
//后面的都需要更新
for (let i = index + 1; i < positions.length; i++) {
positions[i]!.top = positions[i - 1]!.bottom;
positions[i]!.bottom = positions[i]!.top + positions[i]!.height;
}
}
3.3 性能优化
3.3.1 节流处理
滚动事件使用lodash.throttle进行节流处理,默认100ms间隔。
3.3.2 预加载机制
根据前后预加载数量(prevCount/nextCount)渲染额外项,减少快速滚动时的白屏现象。
该组件可以轻松处理包含上万条数据的列表,同时保持流畅的滚动体验,是处理大数据列表渲染的理想解决方案。
团队介绍
「智慧家技术平台-智家APP开发」通过持续迭代演进移动端一站式接入平台为三翼鸟APP、智家APP等多个APP提供基础运行框架、系统通用能力API、日志、网络访问、页面路由、动态化框架、UI组件库等移动端开发通用基础设施;通过Z·ONE平台为三翼鸟子领域提供项目管理和技术实践支撑能力,完成从代码托管、CI/CD系统、业务发布、线上实时监控等Devops与工程效能基础设施搭建。