开头瞎扯淡
作为一个前端页面仔,经常会需要运用长列表去展示大量数据,虽然说已经用了分页展示,但是极端情况下页面足够多的时候数据就会特别多然后出现卡顿等性能问题。当然有的人觉得自己平时要处理的数据没那么多,用不上这个。但是吧俗话说的好,有些东西你不一定用得上,但是你不能不会。性能优化是前端进阶的一个重点,咱不一定次次都用,但咱不能不会呀。
比如微信小程序也在自己的官方文档里有如下的建议,当然你说你不接受微信的建议当我没说。。。
在此之前,自己在网上查过不少资料和文章。感觉都不是很靠谱。直到后来看到了云中桥大佬的「前端进阶」高性能渲染十万条数据(虚拟列表) 。在此特别感谢云中桥大佬提供的思路。我也只是在上面稍作改良。
废话不多说,直接开干。
思路和原理
先来唠唠实现的思路。要想防止节点过多,就要控制渲染节点的数量。视窗就那么大,渲染十万条数据一次也只能看到窗口里的那点东西,所以我们只用动态渲染一部分即可,当然这会涉及的频繁的操作DOM,但是跟同时渲染几十万上百万数据相比,这点消耗不算什么。
(友情提示:list_scroll、list、list_container皆为后面实现代码中的类名,此处用类名指代具体的部分)
具体实现原理如图:
虚拟列表list_scroll只是为了触发滑动实际上就是个总数据高度相同但是没有任何内容的DIV,所以会设置z-index=-1把它在页面上遮挡住。
通过获取虚拟列表list_scroll的滚动高度,然后计算出实际列表list需要移动的位置来模拟出实际的滚动效果。你能看到的滚动其实是实际列表list不断的往下挪动形成的,虚拟列表list_scroll每往上滑动一个节点的高度,实际列表list就往下移动一个节点的高度,反之亦然。
实现代码
<template>
<div>
<div ref="list" class="list_container" @scroll="scrollEvent">
<div class="list_scroll" :style="{ height: listHeight + 'px' }"></div>
<div
class="list"
:style="{ transform: `translateY(${this.startOffset}px)` }"
>
<div class="list_item" v-for="item in visibleData" :key="item">
{{ item }}
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
listData: [], //总数据
//每项高度
itemHeight: 200,
//可视区域高度
screenHeight: 0,
//偏移量
startOffset: 0,
//起始索引
start: 0,
//结束索引
end: null,
};
},
computed: {
//列表总高度
listHeight() {
return this.listData.length * this.itemHeight;
},
//可显示的列表项数
//+2是为了多渲染两个节点,尽量减少快速滑动的时候白屏的影响
visibleCount() {
return Math.ceil(this.screenHeight / this.itemHeight) + 2;
},
//获取真实显示列表数据
visibleData() {
return this.listData.slice(
this.start,
Math.min(this.end, this.listData.length)
);
},
},
created() {
this.onPullDownRefresh();
},
mounted() {
this.screenHeight = this.$refs.list.clientHeight;
this.start = 0;
this.end = this.start + this.visibleCount;
},
methods: {
scrollEvent() {
//当前滚动位置
let scrollTop = this.$refs.list.scrollTop;
if (this.screenHeight + scrollTop == this.listHeight) {
//触底加载
this.onReachBottom();
} else if (scrollTop == 0) {
//触顶刷新
this.onPullDownRefresh();
}
//防止多次计算 只有在需要变化时再计算
if (this.start != Math.floor(scrollTop / this.itemHeight)) {
this.start = Math.floor(scrollTop / this.itemHeight);
this.end = this.start + this.visibleCount;
this.startOffset = scrollTop - (scrollTop % this.itemHeight);
}
},
//下拉刷新(演示demo代码,实际应该是请求分页接口获取数据)
onPullDownRefresh() {
let d = [];
for (let i = 1; i <= 10; i++) {
d.push(i);
}
this.listData = d;
},
//上拉加载(演示demo代码,实际应该是请求分页接口获取数据)
onReachBottom() {
let d = [];
let k = this.listData.length;
for (let i = k + 1; i <= 10 + k; i++) {
d.push(i);
}
this.listData = this.listData.concat(d);
},
},
};
</script>
<style scoped>
.list_container {
height: 700px;
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
}
.list_scroll {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.list {
left: 0;
right: 0;
top: 0;
position: absolute;
text-align: center;
}
.list_item {
color: #333;
height: 200px;
line-height: 200px;
box-sizing: border-box;
border-bottom: 1px solid #999;
}
</style>
暂时存在的亿点点问题
- 如果只渲染可见视图高度数量的节点,在快速刷新的时候会有白屏影响,所以会多渲染几个节点避免这种情况出现;
- 使用此种方法在体验上稍稍要比原生的滚动效果差一点(不知道有没有更好的解决方案如果有还希望大佬不吝赐教),所以往往是在有需要的时候才使用。具体取舍取决于项目需求;
其他方案
类似的滚动组件,现在市面上已经有很多成熟的第三方框架可以直接使用了,体验也更好。例如: