背景
有时候数据量大,页面请求数量过多会造成不好的体验,比如接口响应时间慢、并发请求多,渲染慢等等。最近就遇到了类似的优化需求,主要是三种滚动分页加载、虚拟列表、延迟加载,在这里记录一下他们的使用场景、以及个人实现的总结。欢迎大佬指正~
一、滚动分页加载
场景:
滚动分页加载主要是为了在首次渲染时更快的加载数据,在一些没有分页器但是数据量较多的页面使用。需要后端接口支持分页。
思路:
初始的时候请求多少数据?1、可以默认设置一个pageSize(页面大小)2、也可以根据视口的高度来计算一个初始的pageSize,需要监听窗口的大小改变。
什么时候加载下一页数据?监听滚动事件,当滚动条到达底部时,(可见高度(clientHeight)+已滚动的距离(scrollTop) >= 元素滚动区域的高度(scrollHeight))需要加载下一页的数据,并将数据拼接到至已有数据中。
刷新数据怎么操作?如需要刷新列表,则要将已有数据清空,并将当前的页码置为1。
实现:
listenScrollFn() {
const domRef = this.$refs.domRef as HTMLElement;
domRef.addEventListener('scroll', async (e: any) => {
const event = e.target;
// 判断是否滚动到底部
if (event.clientHeight + event.scrollTop + 5 >= event.scrollHeight && !this.loading)
{
// 判断当前是否已经是最后一页
if (this.pagination.currentPage * this.pagination.pageSize < this.totalCount) {
// 加载下一页数据
this.pagination.currentPage++;
await this.loadData();
} else return;
}
});
}
二、延迟加载
场景:
延迟加载主要是避免当前页面首次渲染时并发请求接口数量过多,可以优化为先请求可视区域部分,再延迟加载剩余部分,这样可以大大减少接口并发请求同时优化首屏渲染速度。
思路:
关于IntersectionObserver?IntersectionObserver 对象,用于推断某些节点是否可以被用户看见、有多大比例可以被用户看见。
主要使用到的属性和方法:
observe() 开始观察
unobserve()停止观察
disconnect()关闭观察
intersectionRatio 目标元素可见比例
什么时候开始加载数据?根据intersectionRatio来判断当前元素是否已经出现在视口中或者出现的比例为多少,来决定是否加载相关数据。
刷新数据怎么操作?调用observe() 重新开始观察即可
实现:
也可以封装为自定义指令,但是对于如果要刷新数据的场景,可能不太方便。
lazyLoadObserver: any = {};
listenViewPort(fn: any) {
this.lazyLoadObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry, index) => {
let lazyDom = entry.target;
// intersectionRatio 目标元素可见比例
if (entry.intersectionRatio > 0) {
fn();
observer.unobserve(lazyDom);
}
});
});
this.lazyLoadObserver.observe(this.$refs.domRef as Element);
}
beforeDestroy() {
this.lazyLoadObserver.disconnect()
}
三、虚拟列表
场景:
虚拟列表主要是为了优化长列表的渲染速度,只渲染可视区域部分,如当请求到的数据量很大时,一次性渲染可能会导致卡顿,体验不佳。与第一种的区别是虚拟渲染页面固定渲染元素的个数,不会随着滚动使得页面元素变多,同时不需要后端接口支持分页。
思路:
元素结构?
可视区域能渲染多少个元素?
visibleCount= 通过容器的高度/列表每一项的高度即可
如何截取当前要渲染的数据?
startIndex=** 当前滚动的距离scrollTop/**列表每一项的高度itemHeight
从startIndex开始截取visibleCount条数据渲染即可
滚动后要做哪些操作?
1、计算出要渲染的数据
2、将列表容器 list-wrap-content 向下移动,保证所有数据在视口中
实现:
<div class="list-wrap" ref="listWrapRef" @scroll="getVisibleData">
<div
class="list-wrap-scroll"
:style="{ height: `${listData.length * this.itemHeight}px` }"
></div>
<div class="list-wrap-content" ref="listWrapContentRef">
<div class="list-wrap-content-item" v-for="item in visibleData" :key="item">{{ item }}</div>
</div>
</div>
data() {
return {
listData: new Array(200).fill(1).map((item, index) => `item-${index + 1}`),
itemHeight: 50,
listHeight: 600,
visibleData: [],
};
},
computed: {
visibleCount() {
return ~~(this.listHeight / this.itemHeight);
},
},
methods: {
getVisibleData() {
const scrollTop = this.$refs.listWrapRef.scrollTop;
const startIndex = ~~(scrollTop / this.itemHeight);
const endIndex = startIndex + this.visibleCount;
// 截取数据
this.visibleData = this.listData.slice(startIndex, endIndex);
// 移动内容
this.$refs.listWrapContentRef.style.transform = `translate3d(0,${
startIndex * this.itemHeight
}px,0)`;
},
},
created() {},
mounted() {
this.getVisibleData();
},
.list-wrap {
height: 600px;
width: 360px;
border: 1px solid;
overflow: auto;
position: relative;
&-scroll {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
&-content {
&-item {
height: 50px;
color: blue;
line-height: 50px;
text-align: center;
border: 1px solid lightblue;
box-sizing: border-box;
}
}
}
版权归原作者 陈Chen. 所有, 如有侵权,请联系我们删除。