不是,是谁还在傻傻遍历生成成千上万个DOM?

虚拟列表赶紧用起来,轻轻松松解决超多重复DOM节点造成的卡顿~

以下有三种不同级别的虚拟列表,分别针对生成的重复DOM节点是固定高度、不同高度和动态变化高度~

1.基础段位:固定高度

tutieshi_640x594_13s

虚拟列表的原理其实就是以下几条:

①一个外层盒子提供滚动事件

②外层盒子中装的第一个是platform,一个空盒子,这个空盒子的高度是列表如果真实渲染应该有的高度,作用是为了撑开外层盒子,提供滚动条

②外层盒子中装的第二个是展示列表盒子,这个盒子中放置所有现在应该出现在页面上的列表项和前后缓冲区。该盒子采用绝对定位,top值根据滚动位置实时改变,让展示列表不论怎么滚动一直出现在页面上

④酌情给一些在页面展示之前之后的缓冲区,防止因为用户滚动过快而造成的空白

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
<template>
<div class="virtualBox" @scroll="scrollEvent" ref="virtualBox">
<div class="platform" :style="{ height: platformHeight + 'px' }">
</div>
<div class="trueBox" :style="{ top: top + 'px' }">
<div v-for="(key, value) in showData" class="itemBox" ref="itemBox">
<button>看起来{{ key }} 其实我是{{ value }}</button>
</div>
</div>
</div>
</template>

<script>
export default {
name: 'WebFront',
data() {
return {
listData: [],//真实列表Data
count: 100,//真实列表项的个数,我这里为了展示手动赋值,真是使用直接获取Data长度即可
platformHeight:0//platform的高度
showData: [],//被展示的列表Data
startIndex: 0,//开始截取listData的Index
showNum: 1,//页面高度可以展示几个列表项
top: 0,//展示列表盒子绝对定位的top值
catchFrontNum: 4, //前缓冲区的数量
catchBackNum: 4,//后缓冲区的数量
itemHeight: 0,//列表项的高度
}
},
methods: {
scrollEvent(e) {
let scrollTop = e.target.scrollTop//获取滚动的距离
this.startIndex = Math.ceil(scrollTop / this.itemHeight)//滚动距离除以列表项的高度得到应该展示的列表项Index
this.startIndex =
this.startIndex < this.catchFrontNum
? 0
: this.startIndex - this.catchFrontNum//设置前缓冲区
//对展示的数组进行截取
this.showData = this.listData.slice(
this.startIndex,
this.startIndex + this.showNum + this.catchBackNum + this.catchFrontNum
)
//绝对定位的展示列表项盒子的top值
this.top = this.startIndex * this.itemHeight
},
},
mounted() {
const virtualBox = this.$refs.virtualBox // 获取到最外层盒子
let itemBox = document.getElementsByClassName('itemBox')[0]
this.itemHeight = itemBox.offsetHeight//获取列表项
this.platformHeight = this.count * this.itemHeight
this.showNum = Math.ceil(virtualBox.clientHeight / this.itemHeight)//外层盒子的可视高度除以列表项高度可以得到展示数量
this.showData = this.listData.slice(
this.startIndex,
this.startIndex + this.showNum + this.catchBackNum+ this.catchFrontNum
)
},
created() {
//做一些假数据用于展示
let i = 0
for (i = 0; i < 100; i++) {
this.listData[i] = '我是' + i
}
this.showData = this.listData.slice(0, 20)
},
}
</script>
<style scoped>
.trueBox {
position: absolute;
top: 0;
}
.itemBox {
height: 50px;
background-color: green;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
}
.platform {
background-color: red;
}
.virtualBox {
height: 85vh;
overflow: scroll;
position: relative;
}
</style>

2.进阶段位:不同高度

tutieshi_640x594_13s

与固定高度不同,列表项的高度是不固定的,所以会出现以下这些难点:

①无法通过页面高度除以列表项高度得到应当展示的数量,也就是展示列表的长度

②无法通过滚动了的高度scrollTop除以列表项高度得到此时应该展示的列表项Index

③无法直接通过ListData的长度乘以列表项高度得到platform的高度

对于以上难点我们的解决方案:

①设置一个预告高度,用于计算页面展示的数量,该预估高度建议偏小,避免出现页面展示数量不够的情况

②设置一个position数组,计算并存储每一个列表项的top\bottom\height值,通过比较scrollTop和列表项的position可以得到此时应该展示的列表项Index

③通过position数组获取最后一个列表项的bottom值,即为platform的高度

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
<template>
<div class="virtualBox" @scroll="scrollEvent" ref="virtualBox">
<div class="platform" :style="{ height: platformHeight + 'px' }">
<!-- 这是假的容器,作用:撑开盒子和提供滚动效果 -->
</div>
<div class="trueBox" :style="{ top: top + 'px' }">
<div
v-for="(item, key) in showData"
class="itemBox"
ref="items"
:id="item.id"
:key="item.id"
>
看着第{{ key }}个 其实第{{ item.id }}个
{{ item.value }}
</div>
</div>
</div>
</template>

<script>
export default {
name: 'WebFront',
data() {
return {
position: [],
listData: [],
platformHeight: 0,
count: 100,
scrollTop: 0,
showData: [],
startIndex: 0,
showNum: 0,
top: 0,
estimatedItemHeight: 100,//预设高度
}
},
methods: {
updateItemsSize() {
//更新列表项高度
let nodes = this.$refs.items
nodes.forEach((node) => {
let rect = node.getBoundingClientRect()
let height = rect.height
let index = parseInt(node.id)
let oldHeight = this.position[index].height
let dValue = oldHeight - height
if (dValue) {
this.position[index].bottom = this.position[index].bottom - dValue
this.position[index].height = height
for (let k = index + 1; k < this.position.length; k++) {
this.position[k].top = this.position[k - 1].bottom
this.position[k].bottom = this.position[k].bottom - dValue
}
this.platformHeight = this.position[this.position.length - 1].bottom
}
})
},
findStartIndex(scrollTop, list) {
//根据滚动高度scrollTop找到此时的startIndex
for (let i = 0, len = list.length; i < len; i++) {
if (list[i].top > scrollTop) {
return i - 1
}
}
return list.length - 1
},
scrollEvent(e) {
this.updateItemsSize()
this.scrollTop = e.target.scrollTop
let index = this.findStartIndex(this.scrollTop, this.position)
this.startIndex =
index < this.listData.length - 1 - this.showNum
? index
: this.listData.length - 1 - this.showNum
//至少保留showNum个列表项
this.showData = this.listData.slice(
this.startIndex,
this.startIndex + this.showNum + 2
)
this.top = this.position[this.startIndex].top
},
createString(num) {
let str = ''
for (let i = 0; i < num; i++) {
str += 'aa'
}
return str
},
},
mounted() {
this.position = this.listData.map((item, index) => ({
index,
top: index * this.estimatedItemHeight,
bottom: (index + 1) * this.estimatedItemHeight,
height: this.estimatedItemHeight,
}))
this.platformHeight = this.position[this.position.length - 1].bottom
this.showNum = Math.ceil(
this.$refs.virtualBox.clientHeight / this.estimatedItemHeight
)
this.showData = this.listData.slice(
this.startIndex,
this.startIndex + this.showNum + 2
)
},
created() {
let i = 0
for (i = 0; i < 100; i++) {
this.listData[i] = {}
this.listData[i].value = this.createString(
Math.floor(Math.random() * 100)
)
this.listData[i].id = i
}
this.showData = this.listData.slice(0, 20)
},
}
</script>
<style scoped>
.trueBox {
position: absolute;
top: 0;
}
.itemBox {
background-color: green;
display: block;
line-height: 100%;
word-break: break-all;
width: 100px;
padding: 10px;
border: 2px purple solid;
}
.platform {
background-color: red;
}
.virtualBox {
height: 85vh;
overflow: scroll;
position: relative;
}
</style>

3.高阶段位:变化高度

这种情况可能出现在比如列表项因为太长而设置了展开/收缩按钮,此时列表项的高度是动态发生变化的,这种情况和上一种情况差不多,区别只在于这种情况只需要在点击按钮的时候将position更新即可~所以在这里不做代码演示啦

总结

了解了原理要写出来还是不难的~但我个人感觉有的时候前端的进阶难就难在止步于此,现在的浏览器性能好,可能写N个DOM都不会卡顿,很难会有觉悟自己去写一个虚拟列表来试试看性能是不是更好。希望自己永不止步,永远进步