虚拟列表赶紧用起来,轻轻松松解决超多重复DOM节点造成的卡顿~
以下有三种不同级别的虚拟列表,分别针对生成的重复DOM节点是固定高度、不同高度和动态变化高度~
1.基础段位:固定高度
虚拟列表的原理其实就是以下几条:
①一个外层盒子提供滚动事件
②外层盒子中装的第一个是platform,一个空盒子,这个空盒子的高度是列表如果真实渲染应该有的高度,作用是为了撑开外层盒子,提供滚动条
②外层盒子中装的第二个是展示列表盒子,这个盒子中放置所有现在应该出现在页面上的列表项和前后缓冲区。该盒子采用绝对定位,top值根据滚动位置实时改变,让展示列表不论怎么滚动一直出现在页面上
④酌情给一些在页面展示之前之后的缓冲区,防止因为用户滚动过快而造成的空白
vue
<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.进阶段位:不同高度
与固定高度不同,列表项的高度是不固定的,所以会出现以下这些难点:
①无法通过页面高度除以列表项高度得到应当展示的数量,也就是展示列表的长度
②无法通过滚动了的高度scrollTop除以列表项高度得到此时应该展示的列表项Index
③无法直接通过ListData的长度乘以列表项高度得到platform的高度
对于以上难点我们的解决方案:
①设置一个预告高度,用于计算页面展示的数量,该预估高度建议偏小,避免出现页面展示数量不够的情况
②设置一个position数组,计算并存储每一个列表项的top\bottom\height值,通过比较scrollTop和列表项的position可以得到此时应该展示的列表项Index
③通过position数组获取最后一个列表项的bottom值,即为platform的高度
vue
<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都不会卡顿,很难会有觉悟自己去写一个虚拟列表来试试看性能是不是更好。希望自己永不止步,永远进步