虚拟列表、虚拟滚动
虚拟列表是一种优化长列表渲染性能的技术,通过仅渲染可视区域内的元素,减少 DOM 节点数量和渲染开销。以下是其实现的核心原理与步骤:
1. 核心原理
- 视窗渲染:只渲染用户视野内的元素,动态替换滚动时进入视野的元素。
- 滚动占位:用空白区域(或占位符)模拟完整列表的滚动长度。
- 动态计算:根据滚动位置和元素尺寸,计算当前应渲染的元素范围。
- 定位偏移:通过
transform
或absolute
定位,让元素出现在正确位置。
2. 实现步骤(以固定高度为例)
(1) HTML 结构设计
html
<!-- 容器负责滚动,高度由外部定义 -->
<div class="virtual-container">
<!-- 总高度撑起滚动条 -->
<div class="scroll-area" :style="{ height: totalHeight + 'px' }">
<!-- 可视区域,绝对定位渲染当前项 -->
<div class="visible-items" :style="{ transform: `translateY(${offset}px)` }">
<div v-for="item in visibleItems" :key="item.id" class="item">{{ item.text }}</div>
</div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
(2) CSS 样式
css
.virtual-container {
overflow-y: auto; /* 允许滚动 */
height: 600px; /* 容器可视高度 */
}
.scroll-area {
position: relative; /* 相对定位,用于子元素定位 */
}
.visible-items {
position: absolute; /* 绝对定位,根据滚动偏移计算位置 */
top: 0;
left: 0;
width: 100%;
}
.item {
height: 50px; /* 固定高度项 */
border-bottom: 1px solid #eee;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(3) JavaScript 逻辑
javascript
new Vue({
data: {
items: [], // 所有数据(假设1000条)
itemHeight: 50, // 每项固定高度
visibleCount: 0, // 可视区能显示多少项
startIndex: 0, // 起始索引
offset: 0 // Y轴偏移量
},
computed: {
// 可渲染的总高度(撑起滚动条)
totalHeight() {
return this.items.length * this.itemHeight;
},
// 当前可视区域的数据切片
visibleItems() {
return this.items.slice(
this.startIndex,
this.startIndex + this.visibleCount
);
}
},
mounted() {
// 初始化可视区能容纳的项数
this.visibleCount = Math.ceil(this.$el.clientHeight / this.itemHeight);
// 监听滚动事件
this.$el.addEventListener('scroll', this.handleScroll);
},
methods: {
handleScroll(e) {
const scrollTop = e.target.scrollTop;
// 计算当前起始索引(被卷走了多少个项)
this.startIndex = Math.floor(scrollTop / this.itemHeight);
// 计算偏移量(让元素出现在正确位置)
this.offset = this.startIndex * this.itemHeight;
}
},
beforeDestroy() {
this.$el.removeEventListener('scroll', this.handleScroll);
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
3. 动态高度优化
若列表项高度不固定,需 实时测量 + 缓存结果,算法复杂度更高:
(1) 预估初始高度
javascript
// 假设初始预估高度为 100px
estimatedHeight: 100,
// 总高度(基于预估值)
totalHeight() {
return this.items.length * this.estimatedHeight;
}
1
2
3
4
5
6
2
3
4
5
6
(2) 监听项的实际高度
javascript
// 记录每个项的实际高度
heights: [],
// 更新高度缓存
updateHeight(index, height) {
this.$set(this.heights, index, height);
// 重新计算总高度(实际值)
this.totalHeight = this.heights.reduce((sum, h) => sum + (h || this.estimatedHeight), 0);
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
(3) 根据实际高度计算偏移
需维护一个 位置数组(每个项的累计高度),如:
javascript
// positions存储每个项的起始位置和高度
positions.push({
top: prevItemTop + prevItemHeight,
height: currentItemHeight,
bottom: prevItemTop + prevItemHeight + currentItemHeight
});
1
2
3
4
5
6
2
3
4
5
6
(4) 二分查找当前滚动位置对应的索引
根据 scrollTop
快速查找起始索引:
javascript
function findStartIndex(scrollTop) {
let low = 0, high = positions.length;
while (low < high) {
const mid = Math.floor((low + high) / 2);
if (positions[mid].bottom < scrollTop) {
low = mid + 1;
} else {
high = mid;
}
}
return low;
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
4. 性能优化技巧
- 缓冲渲染:多渲染几个屏幕外的元素(如上 / 下各多 5 个),避免滚动白屏。javascript
// 计算时增加缓冲区 startIndex = Math.max(0, startIndex - buffer); endIndex = Math.min(items.length - 1, endIndex + buffer);
1
2
3 - 节流滚动事件:限制
scroll
触发频率,减少计算次数。javascripthandleScroll: throttle(function(e) { /* ... */ }, 50)
1 - 使用
transform
替代top
:transform
不触发重排,性能更好。
5. 适用场景与工具库
- 适用:长列表(如聊天记录、表格数据、社交动态流)。
- 工具库:
- React:
react-window
、react-virtualized
- Vue:
vue-virtual-scroller
、vue-virtual-scroll-list
- React:
通过虚拟列表,可大幅提升长列表的渲染性能与用户体验!
举例:Vue 虚拟滚动组件实现