场景引入
为什么需要用到高性能虚拟列表+节流+分页请求的优化?
当有场景需求为需要将大量数据(10000条)呈现在一页上,我们不断下拉访问,页面中有大量的数据列表的时候,用户会不会有不好的体验?会不会出现滚动不流畅而卡顿的情况?会不会因卡顿而出现短暂的白屏现象(数据渲染不成功)?
通过微信开发者工具自带的调试器->Network页面,我们可以观察到当有长列表时如果不使用高性能虚拟列表+节流+分页请求的优化,会出现以下问题:
- FPS:每秒帧数,图表上的红色块表示长时间帧,很可能会出现卡顿。
- CPU:CPU消耗占用,实体图越多消耗越高。
- NET:网络请求效率低,一次性请求10000条的渲染效率远远低于分1000次,每次请求10条数据
- 内存:滑动该列表时明显能看到内存消耗大。
总结:如果需要将大量数据(10000条)呈现在一页上,可以通过高性能虚拟列表+节流+按需请求分页数据并追加显示。
优化的具体实现可拆分为以下需求(将一个大问题拆分为一个个小问题并逐个去解决):
-
不把长列表数据一次性全部直接显示在页面上。
-
截取长列表一部分数据用来填充屏幕容器区域。
-
长列表数据不可视部分使用使用空白占位填充。
-
监听滚动事件根据滚动位置动态改变可视列表。
-
监听滚动事件根据滚动位置动态改变空白填充。
-
分页从服务器请求数据,将一次性请求所有数据变为滚动到底部才再次向服务器发送获取数据的请求
开始实战
此次实例我设定每个元素的固定高度为210rpx
(1)首先计算屏幕内的容积最大容量(即屏幕一次性可以容纳多少个高度为210rpx的元素)
调用wx.createSelectorQuery()的api
上图是id为scrollContainer的组件,滚动时触发函数handleScroll()
// 计算容器的最大容积,onReady中触发,即初次渲染时触发
getContainSize() {
wx.createSelectorQuery().select('#scrollContainer').boundingClientRect(function (rect) {
rect.id // 节点的ID
rect.dataset // 节点的dataset
rect.left // 节点的左边界坐标
rect.right // 节点的右边界坐标
rect.top // 节点的上边界坐标
rect.bottom // 节点的下边界坐标
rect.width // 节点的宽度
rect.height // 节点的高度
}).exec((option) => {
// console.log(~~(option[0].height / this.data.oneHeight) + 2);
this.data.containSize = ~~(option[0].height / this.data.oneHeight) + 2;
})
},
调用api后返回的option中的height就是小程序页面的视口高度,除以oneHeight(210rpx)就是能容纳的个数,用~~
来对结果进行向下取整(实际能容纳的应+1),由于在滚动时会出现上一个元素在上边界还没完全消失,第四个元素就从下边界进入视口了,因此最大容纳量应再+1。即实际容纳量应该如图最后一行代码所示+2.
(2)监听滚动事件动态截取数据
监听用户滚动、滑动事件,根据滚动位置,动态计算当前可视区域起始数据的索引位置 startIndex,再根据containsize,计算结束数据的索引位置 endIndex,最后根据 startIndex与endIndex截取长列表所有数据a11Datalist 中需显示的数据列表 showDatalist。
PS:下列代码中函数handleScroll()最下面的this.setDataStartIndex(data);
才是滚动时真正进行的滚动事件动态截取数据,上面那些代码用途在文章后面部分再详细介绍(节流)。
// 定义滚动行为事件方法
handleScroll(data) {
if (this.data.isScrollStatus) {
this.data.isScrollStatus = false;
// 节流,设置一个定时器,1秒以后,才允许进行下一次scroll滚动行为
let mytimer = setTimeout(() => {
this.data.isScrollStatus = true;
clearTimeout(mytimer);
}, 17)
this.setDataStartIndex(data);
}
},
// 执行数据设置的相关任务, 滚动事件的具体行为
setDataStartIndex(data) {
// console.log("scroll active")
this.data.startIndex = ~~(data.detail.scrollTop / this.data.oneHeight);
// 通过scrollTop滑动后距离顶部的高度除以每个元素的高度,即可知道目前到第几个元素了
this.setData({
showDataList: this.data.allDatalist.slice(this.data.startIndex, this.data.endIndex)
})
// 动态截取实际拥有10000条数据的数组中下标为startIndex到endIndex的数组出来呈现在前端页面上
// 容器最后一个元素的索引
if (this.data.startIndex + this.data.containSize <= this.data.allDatalist.length - 1) {
this.setData({
endIndex: (!this.data.allDatalist[this.data.endIndex]) ? this.data.allDatalist.length - 1 : this.data.startIndex + this.data.containSize
// 滚动到底部了吗,是的话那就将endIndex设置为9999,不然的话设置为startIndex+视口最大容量
})
} else {
console.log("滚动到了底部");
this.data.pageNumNow++;
// 例如一次性从数据库拿10条数据赋值到allDataList,如果滚动到底部(即allDataList所有数据都已经呈现了),那就再次向服务器发送请求获取数据库中的下10条数据
this.addMes(); // 该函数内就写你实际向数据库请求时的代码,请求成功后拼接到allDataList即可
console.log(this.data.allDatalist.length)
}
},
(3)使用计算属性动态设置上下空白占位
我们设置了根据容器滚动位移动态截取ShowDatalist 数据,现在我们滚动一下发现滚动2条列表数据后,就无法滚动了,这个原因是什么呢?
在容器滚动过程中,因为动态移除、添加数据节点丢失,进而强制清除了顶部列表元素DOM节点,导致滚动条定位向上移位一个列表元素高度,进而出现了死循环根据
startIndex和endIndex的位置,使用计算属性,动态的计算并设置,上下空白填充的高度样式blankFi11Sty1e,使用padding或者margin 进行空白占位都是可以的
PS:由于小程序没有computed,所以为了使用计算属性,得另外引入封装好computed的包,引入computed组件的教程我放在最后,我使用的是官方推荐的computed,且注意使用该插件时,不要加this -> this.data
,直接data即可
computed: {
// 定义上空白高度
topBlankFill(data) {
// console.log("change")
return data.startIndex * data.oneHeight;
},
// 定义下空白高度
BottomBlankFill(data) {
return (data.allDatalist.length - data.endIndex) * data.oneHeight;
},
// 定义一个 待显示的数组列表元素
showDataList(data) {
// console.log(data.allDatalist.slice(data.startIndex, data.endIndex))
return data.allDatalist.slice(data.startIndex, data.endIndex)
},
},
(4)下拉置底自动请求加载数据
下方代码中setDataStartIndex()
函数末尾的if-else便是判断是否已经滚动到现有全部数据的allDataList数组是否已经滚动到底部,全部呈现完了。
如果是那就执行else部分,请求的页数pageNumNow+1,然后调用addMes()请求数据,然后将新请求到的数据进行拼接到allDataList上
// 执行数据设置的相关任务, 滚动事件的具体行为
setDataStartIndex(data) {
// console.log("scroll active")
this.data.startIndex = ~~(data.detail.scrollTop / this.data.oneHeight);
this.setData({
showDataList: this.data.allDatalist.slice(this.data.startIndex, this.data.endIndex)
})
// 容器最后一个元素的索引
if (this.data.startIndex + this.data.containSize <= this.data.allDatalist.length - 1) {
this.setData({
endIndex: (!this.data.allDatalist[this.data.endIndex]) ? this.data.allDatalist.length - 1 : this.data.startIndex + this.data.containSize
})
} else {
console.log("滚动到了底部");
this.data.pageNumNow++;
this.addMes();
console.log(this.data.allDatalist.length)
}
},
// 根据接口数据来给数组添加真实数据
addMes: function () {
this.list()
.then(res => {
// console.log(res.result.data);
// 将接口获取到得所有数据存储起来
this.data.allDatalist = this.data.allDatalist.concat(res.result.data);
// 设置初始显示列表
this.setData({
showDataList: this.data.allDatalist.slice(0, 5)
})
})
},
// 以下为获取数据
list: function () {
let pageNum = this.data.pageNumNow;
let pageSize = 20;
// console.log('当前请求的页码为:' + pageNum);
return new Promise((resolve, reject) => {
wx.cloud.callFunction({
name: 'teacher',
data: { action: 'list', pageNum, pageSize }
}).then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})
},
到此处,高性能虚拟列表+分页请求的优化已经搞定了,下面开始节流的优化
(5)滚动事件节流定时器优化
由于监听滚动事件触发对应函数方法的频率是极高的,因此页面节流优化是必须的。
方法:在data中声明一个属性scro11State用来记录滚动状态,只有scro11State值为true的时候才会具体执行
PS:下面代码中定时器设置为17ms的原因是,由于小程序没有web的requestAnimationFrame的api,无法作滚动事件节流请求动画帧优化,因此可以手动计算显示器的帧率,大概一帧在17ms,因此定时器设置为17ms
// 定义滚动行为事件方法
handleScroll(data) {
//节流部分代码
if (this.data.isScrollStatus) {
this.data.isScrollStatus = false;
// 节流,设置一个定时器,1秒以后,才允许进行下一次scroll滚动行为
let mytimer = setTimeout(() => {
this.data.isScrollStatus = true;
clearTimeout(mytimer);
}, 17)
//节流部分代码
this.setDataStartIndex(data);
}
},
到这为止,节流也搞定啦,觉得从中学到了许多的小伙伴不妨点个赞。
小程序如何引入computed计算属性请参考我的这篇文章:https://developers.weixin.qq.com/community/develop/article/doc/000a4442bd44c84e740d6b6b051413
在后续我也会将高性能虚拟列表+节流的优化封装成一个插件,给小伙伴们直接使用,欢迎关注我以便及时获取到我文章的更新~
在这里也推荐一篇防抖和节流的性能优化知识介绍的文章:https://segmentfault.com/a/1190000018428170
👍学到啦
学到啦
学习到了!感谢分享~
能否给个代码片段或者源码看看,万分感谢~
赞