一次性需要渲染大量的数据浏览器渲染过慢甚至渲染崩溃解决方案
本文前半部分提供思路供学习
着急解决的朋友可以直接滑最底部查看目前最快解决方法
1.宏任务分批渲染节点
<div id="list"></div>
<script>
let total = 10000
let id = 0
let index = 20
function load () {
index += 20
if (index < total) {
let list = document.getElementById('list')
for (let index = 0; index < 20; index++) {
let li = document.createElement('li')
li.innerHTML = id++
list.appendChild(li)
}
load()
}
}
load()
</script>
新版浏览器对文档碎片做过优化这种执行方法还是会在全部执行完毕之后一次性渲染页面
已知JS执行顺序
主线程执行完毕之后 -> 进入微任务队列 -> 渲染页面 -> 宏任务队列 (执行一个宏任务) -> 进入主线程 ....依次轮询
// 宏任务执行
<div id="list"></div>
<script>
let total = 10000
let id = 0
let index = 20
function load () {
index += 20
if (index < total) {
setTimeout(() => {
for (let index = 0; index < 20; index++) {
let li = document.createElement('li')
li.innerHTML = id++
let list = document.getElementById('list')
list.appendChild(li)
}
load()
},0)
}
}
load()
</script>
优化一下 将上边的setTimeout 换成 requestAnimationFrame
看似问题解决了全都渲染出来了,但是dom节点全部都加载了,节点多还是会卡!
2.虚拟列表分屏加载
> 思路
> 设置一个父容器 高度由显示数量*每条行高
> 第一个子容器其高度由每行行高*总数据条数 并设置相对定位实现滚动条
> 第二个子容器 为可视区域不设置高度 完全由数据条数撑开 并绝对定位在可视区域,由于页面是会滚动的上下滚动过程中会有留白,所以容器区域需要渲染三屏的节点,当前展示区上一页下一页,滚动过程中会更流畅
// 创建组件接收每条数据高度 可视区域显示数量 总数据条数
props: {
size: {
type: Number,
default: 40
},
limit: {
type: Number,
default: 8
},
list: {
type: [Array],
default () { return [] }
}
},
// 设置容器
// 可视区域偏移量需计算
<div ref="viewport" class="viewport" @scroll="handleScorll">
<div class="scrollBar" ref="scrollBar"></div>
<div class="scrollList" ref="scrollList" :style="{transform:`translate3d(0,${offest}px,0)`}">
<div v-for="item in visibleData" :key="item.id">
<slot :item='item'></slot>
</div>
</div>
</div>
<style scoped>
.viewport{
position: relative;
width: 100%;
box-sizing: border-box;
overflow-y: scroll;
}
.scrollList{
box-sizing: border-box;
width: 100%;
position: absolute;
top: 0;
left: 0;
}
</style>
// 设置设置容器高度
mounted () {
this.$refs.viewport.style.height = this.size * this.limit + 'px'
this.$refs.scrollBar.style.height = this.list.length * this.size + 'px'
},
data () {
return {
start: 0,
end: this.limit,
offest: 0
}
},
// 计算展示数据
computed: {
prevCount () { // 上一页 开始区间的条数与每屏条显示数取最小值
return Math.min(this.start, this.limit)
},
nextCount () { // 下一页 每屏显示数与 总数据数 - 结束跳数 取最小值
return Math.min(this.limit, this.list.length - this.end)
},
visibleData () { // 当前页 需渲染页面
let start = this.start - this.prevCount
let end = this.end + this.nextCount
return this.list.slice(start, end)
}
},
// 根据滚动事件计算当前展示的数据应该是那个区间
handleScorll () {
let scrollTop = this.$refs.viewport.scrollTop
this.start = Math.floor(scrollTop / this.size)
this.end = this.start + this.limit
// 偏移量
this.offest = this.start * this.size - this.size * this.prevCount
}
********* 分割线 **********
// 组件引入
// item 自定义的子组件 props 可以接收到每条数据的参数
<virtual :limit='8' :size='40' :list='list'>
<item ref="itemRefs" slot-scope="{item}" :item='item'></item>
</virtual>
2.1 高度不确定情况下解决方案
> 1.缓存列表每行高度及距离顶部及底部高度
> 2.动态计算鼠标滑动时当前应展示的列表区间
> 3.在dom节点插入后再计算当前显示的节点实际高度替换缓存中的高度修改可视化位置距离顶部距离
// 先初始缓存列表尺寸及距离顶部底部距离
// mounted 中执行
cacheList () {
this.position = this.list.map((item, index) => ({
height: this.size,
top: index * this.size,
bottom: (index + 1) * this.size
}))
}
// 计算可视区域显示区间
*** 由于鼠标滚动时数据变化很快 dom节点的更新渲染也消耗性能
*** 二分法计算当前显示的开始位置 value 为当前滚动条滚动距离
getStartIndex (value) {
let start = 0
let end = this.position.length - 1
let temp = null
while (start <= end) {
let middleIndex = parseInt((start + end) / 2)
let middleValue = this.position[middleIndex].bottom
if (middleValue === value) {
return middleIndex + 1
} else if (middleValue < value) {
start = middleIndex + 1
} else if (middleValue > value) {
if (temp == null || temp > middleIndex) {
temp = middleIndex
}
end = middleIndex - 1
}
}
return temp
}
// 获取到当前可视化列表区间
this.start = this.getStartIndex(scollTop)
this.end = this.start + this.limit
// 计算偏移量
this.offSet = this.position[this.start - this.prevCount].top
// 偏移量值有可能会是没有的
this.offSet =
this.position[this.start - this.prevCount]?
this.position[this.start - this.prevCount].top:0
// 拿到可视化区域节点之后更新节点
// 拿到所有可视化区域dom获取尺寸信息
updated () {
this.$nextTick(() => {
let nodes = this.$refs.items
if (!(nodes && nodes.length > 0)) {
return
}
nodes.forEach(node => {
let { height } = node.getBoundingClientRect()
let id = node.getAttribute('vid') - 0
let oldHeight = this.position[id].height
let val = oldHeight - height
if (val) {
this.position[id].height = height
this.position[id].bottom = this.position[id].bottom - val
for (let i = id + 1; i < this.position.length; i++) {
this.position[i].top = this.position[i - 1].bottom
this.position[i].bottom = this.position[i].bottom - val
}
}
})
this.$refs.scrollBar.style.height = this.position[this.position.length - 1].bottom
})
},
以上思路可以暂时解决长列表问题
现成的虚拟列表库
npm i vue-virtual-scroll-list -s
<virtual-list style="height: 360px; overflow-y: auto;" :data-key="'uid'" :data-sources="items" :data-component="itemComponent" />uid 为列表数组唯一key
data-sources 为目标列表数据
itemComponent 自定义组件
使用方法原文连接