背景
总有一些特殊的业务场景不能进行后端分页,需要一次性加载所有数据,这时候数据量少则几百条多则上千条,如果只是简单的list可能卡顿还不是很明显,可是加上一些业务DOM节点的话那整体需要渲染的DOM量就很多了,首次渲染会很慢,滚动也会有点卡卡的。
那怎么解决呢?
方案一:前端分页
以PC端为例,常见的后端分页方式就是滚动触底然后去服务器加载新的一页的数据,那现在既然不能后端分页了,可不可以前端自己也去这么模拟这个过程呢。
既然定了思路那就开干,基本思路就是从服务器拿到全量数据之后将他作为备份,真正展示在页面上的最开始只有一页的长度,然后随着滚动,一点一点从备份数据里向这个展示数据列表里塞数据,伪代码如下:
// 展示最新数据
let updateShowData = (list) => {
list.forEach(ele => {
let index = ele.split('-')[0], idx = ele.split('-')[1]
if(idx == 0) {
this.$set(this.patientsData, index, {
...this.patientsBackupData[index],
execNisOrderList: []
})
}
// this.patientsData[index].execNisOrderList[idx] = cloneDeep(this.patientsBackupData[index].execNisOrderList[idx])
this.$set(this.patientsData[index].execNisOrderList, idx, this.patientsBackupData[index].execNisOrderList[idx])
})
this.handleCheckAllChange(true)
console.log('当前-总共', this.loadIndex + '-' + this.orderIndexList.length);
}
// 监听滚动条触发事件
let scrollCallbackFn = () => {
isInViewPortOfOne(this.$refs.nurseTaskContent, loadDataFn)
}
// 前端动态加载显示后端数据
let loadDataFn = () => {
let leaveNum = this.orderIndexList.length - this.loadIndex //剩余未渲染数量
let newIndex = leaveNum >= 2 ? this.loadIndex + 2 : this.loadIndex + leaveNum
if(newIndex > this.loadIndex) {
updateShowData(this.orderIndexList.slice(this.loadIndex, newIndex))
this.loadIndex = newIndex
}
if(this.loadIndex == this.orderIndexList.length) {
console.log('数据加载完成--', this.patientsData)
// 卸载滚动条监听事件
this.$refs.nurseTaskContent.removeEventListener('scroll', scrollCallbackFn)
}
}
// 判断某元素是否滚动到底部
let isInViewPortOfOne = (el, cbFn) => {
// 滚动视口高度(也就是当前元素的真实高度)
let scrollHeight = el.scrollHeight
// 可见区域高度
let clientHeight = el.clientHeight
// 滚动条顶部到浏览器顶部高度
let scrollTop = el.scrollTop
if(clientHeight + scrollTop >= (scrollHeight - 300)){
console.log('滚动条触底了')
cbFn();
}
}
// 滚动条触底加载更多数据
this.$nextTick(() => {
this.$refs.nurseTaskContent.addEventListener('scroll', scrollCallbackFn)
})
然后还可以做一些优化,就是在初始化的时候如果用户没滚动,那就充分利用起浏览器的闲置时间去分页加载,可以用到requestIdleCallback
这个API,伪代码如下:
// 增加监听-空闲时加载医嘱数据
let callBackFn = (deadLine) => {
this.requestIdleCallbackId = window.requestIdleCallback(callBackFn, {timeOut: 5000})
if(deadLine.timeRemaining() > 0 && this.loadIndex && this.loadIndex < this.orderIndexList.length) {
// 做任务
// console.log(`本帧的剩余时间为:${deadLine.timeRemaining()}`)
loadDataFn();
}
else{
window.cancelIdleCallback(this.requestIdleCallbackId)
}
}
this.requestIdleCallbackId = window.requestIdleCallback(callBackFn, {timeOut: 5000})
这种方案在数据结构不复杂的列表下勉强能接受,单是一旦数据量复杂,或者加载完了所有数据之后那就跟一次性加载完所有数据没有差别了,还是会有卡顿,所以体验最好的方案还是虚拟列表。
方案二:虚拟列表
虚拟列表的原理就是始终只渲染固定长度数量的列表,然后动态插值,比如你需要渲染1000条数据,那页面上看到的DOM节点永远只有20条,当你滚动的时候动态改变这20条的内容,一般为了体验更好,最好有一定的缓冲区,也就是在你一屏范围之外还加载一点作为缓冲区,毕竟计算也是需要一点时间的。
vue使用比较多的库有vue-virtual-scroller和[umu-UI](github.com/u-leo/umy-u… 后者用于优化长数据的表格效果比较明显。
当然世面上还有一些其他比较优秀的库还没使用过,有机会页准备研究一下具体的实现源码,研究清楚了再分享出来。