第一、合理使用文档碎片和延迟加载渲染,逐帧绘制十几万条数据
比如有10万条数据,可以将这些数据先进行分片,每20个一帧。
在绘制帧内容时,可以使用 document.createDocumentFragment() 方法将20条数据先逐个放置到一个文档碎片中,然后将整个文档碎片渲染到页面上。
在开始一帧帧绘制时,可以使用 window.requestAnimationFrame() 进行有节奏的延迟绘制,等全部绘制完毕后,再使用 window.cancelAnimationFrame() 停止绘制。
<body>
<ul></ul>
<script>
const ulEl = document.getElementsByTagName('ul')[0]
const total = 100000
const piece = 20
const count = Math.ceil(total/piece)
let currentCount = 0
function add () {
const docFragment = document.createDocumentFragment()
for(let i = 0; i < piece; i++) {
const liNode = document.createElement('li')
liNode.innerText = Math.floor(Math.random() * 100000)
docFragment.appendChild(liNode)
}
ulEl.appendChild(docFragment)
currentCount++
loop()
}
function loop() {
if (currentCount < count) {
// 延迟加载
let d = window.requestAnimationFrame(add)
console.log(d)
} else {
window.cancelAnimationFrame(d)
}
}
loop()
</script>
</body>
补充:
定时器是一个宏任务,会等待ui渲染完后执行下一个宏任务(定时器)
但是上面的分片加载也有缺陷:
导致页面元素过多,造成卡顿,所以可以使用虚拟列表优化
第二、虚拟列表优化
因为只展示可视区域的数据,所以
- 首先需要知道每一项的高度,及希望展示的数据条数,获取可视区域的高度。
- 还要获取数据的总条数,用来算出完整页面的高度,生成一个滚动条
- 在可视区域中,通过截取的方式展示部分需要展示的数据
- 监听滚动事件,如果滚动出2个,则从第三个开始重新渲染
- 但滑动过快时,会出现头部尾部空白的情况,所以设置预加载,比如同时加载前后三屏
当前只实现 每一行定高的情况
引用页面
<template>
<VirtualList :size="40" :remain="8" :initData="initData">
<!-- 因为是截取渲染,所以v-for在virtual-list组件中进行 -->
<VirtualItem slot-scope="{item}" :info="item"></VirtualItem>
</VirtualList>
</template>
<script>
import VirtualList from './virtual/VirtualList'
import VirtualItem from './VirualItem.vue'
const initData = new Array(1000).fill().map((item, i) => ({
value: i
}))
export default {
data() {
return {
initData
}
},
components: {
VirtualList,
VirtualItem
}
}
</script>
自定义item页面
<template>
<h1>{{info.value}}</h1>
</template>
<script>
export default {
props: {
info: Object
},
}
</script>
组件页面
<template>
<div class="container"
ref="container"
@scroll="handleScroll">
<div class="all-page"
ref="allPage"></div>
<ul class="viewport" :style="{transform: `translate3d(0,${offset}px,0)`}">
<li v-for="(item, index) in visibleData"
:key="index"
style="height: 40px; text-align: center; font-size: 26px;border-bottom: 1px solid red; box-sizing: border-box;">
<!-- 定义插槽是为了让使用改组件的用户自定义要展示的内容 -->
<slot :item="item"></slot>
</li>
</ul>
</div>
</template>
<script>
export default {
props: {
size: Number,
remain: Number,
initData: Array
},
data() {
return {
start: 0,
}
},
computed: {
visibleData() {
const start = this.start - this.prevCount
const end = this.end + this.nextCount
return this.initData.slice(start, end)
},
end() {
return this.start + this.remain
},
offset() { // 定义当前可视区域
// return this.start * this.size
return this.start * this.size - this.size * this.prevCount
},
prevCount() { // 前面预留几个
return Math.min(this.start, this.remain)
},
nextCount() { // 后面预留几个
return Math.min(this.remain, this.initData.length - this.end)
}
},
methods: {
handleScroll() {
const scrollTop = this.$refs.container.scrollTop
this.start = scrollTop / this.size // 130/ 40 3.25( 从第四个开始渲染)
}
},
mounted() {
this.$refs.container.style.height = this.size * this.remain + 'px'
this.$refs.allPage.style.height = this.size * this.initData.length + 'px'
}
}
</script>
<style lang="less">
.container {
overflow-y: auto;
position: relative;
background: pink;
}
.viewport {
position: absolute;
top: 0;
left: 0;
width: 100%;
background: powderblue;
}
.all-page {
background: yellowgreen;
}
</style>