产品:现在有10w条数据要展示,不分页,放一个表格里OK不 我 :这一条数据100个属性,浏览器hold不住的 产品:那最多几条 我 :500 ??( 空气凝固3s…) 等我看看再回你

1. 为什么要用虚拟列表

假设我们的列表需要展示1w条数据,我们同时将1w条数据渲染到页面中。看看会花费多少时间。可以粗略的统计到,JS的运行时间为38ms,但渲染完成后的总时间为957ms。当JS执行栈中的事件全部执行完后,才会触发渲染线程对页面进行渲染。

通过Performance来看从执行到渲染结束,时间花在了哪里:

  • Event(click) : 40.84ms
  • Recalculate Style : 105.08ms
  • Layout : 731.56ms
  • Update Layer Tree : 58.87ms
  • Paint : 15.32ms

消耗时间最多的两个阶段是

  • Recalculate Style: 样式计算,浏览器根据css选择器计算哪些元素应该应用哪些规则,确定每个元素具体的样式
  • Layout: 布局,知道元素应用哪些规则之后,浏览器开始计算它要占据的空间大小及其在屏幕的位置

当DOM节点数量越多的时候,每一次滚动的时候,都会消耗大量的时间,内存用来计算布局,最后导致页面卡顿。但是如果我们只考虑能看到的数据,那实际渲染的数据量就会非常的少,减少了大量不必要的重绘。

2. 实现一个虚拟列表

虚拟列表,实际上是在首屏加载的时候,只加载可视区域需要的列表项,当滚动发生时,动态计算拿到可视区域的列表项,并将非可视区域内存在的列表项删除。

创建一个infinite-list-phantom虚拟div,把它的高度设置成总高度,用来撑高整个div,生成滚动条。
滚动时,设置可视列表infinite-listtransform位移属性,为滚动高度,并更新可视列表数据。

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<div ref="list" id="infinite-list-container" @scroll="handleScroll($event)">
<!-- 容器的占位div,高度为总列表高度,用于生成滚动条-->
<div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>

<!-- 可视列表,实际渲染列表 -->
<div class="infinite-list" :style="{ transform: getTransform }">
<div
class="infinite-list-item"
v-for="item in visibleData"
:key="item.id"
:style="{ height: itemSize + 'px', lineHeight: itemSize + 'px' }"
>{{ item.value }}</div>
</div>
</div>
<script>
new Vue({
el: '#infinite-list-container',
data() {
this.itemSize = 30; // 每项高度
return {
listData: [], // 所有列表数据
start: 0, // 开始索引
end: 0, // 结束索引
screenHeight: 0, // 可视区域高度
startOffset: 0, // 偏移量
};
},
computed: {
visibleData() { // 真实显示的列表数据
return this.listData.slice(this.start, Math.min(this.end, this.listData.length))
},
visibleCount() { // 可显示的列表项数
return Math.ceil(this.screenHeight / this.itemSize)
},
listHeight() { // 列表总高度
return this.listData.length * this.itemSize
},
getTransform() { // 偏移量对应的style
return `translate3d(0, ${this.startOffset}px, 0)`
}
},
methods: {
handleScroll() {
// 当前滚动位置
let scrollTop = this.$refs.list.scrollTop;
// 设置可视列表开始,结束索引
this.start = Math.floor(scrollTop / this.itemSize);
this.end = this.start + this.visibleCount;
// 更新偏移量
this.startOffset = scrollTop - (scrollTop % this.itemSize)
}
},
mounted() {
// 初始化假数据
const arr = [];
for (let i = 0; i < 20000; i++) {
arr.push({ id: i, value: `第${i + 1}条数据内容`});
}
this.listData = arr;

setTimeout(() => {
this.screenHeight = this.$el.clientHeight;
this.start = 0;
this.end = this.start + this.visibleCount;
}, 0)
},
})
</script>
<style>
#infinite-list-container {
height: 500px;
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
}
.infinite-list-phantom {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: -1;
}
.infinite-list {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.infinite-list-item {
border-bottom: 1px solid #999;
}
</style>

参考资料