一、前置知识
js是单线程的,为了确保主线程不被长时间阻塞,js中有同步和异步的概念
事件循环机制(event loop)
- 先执行主线程中同步任务
- 遇到异步任务,将异步任务挂到任务队列中
- 主线程执行完毕,开始执行异步任务,异步任务分为宏任务和微任务,先执行微任务,再执行宏任务
- 微任务:
Promise.then()async/await宏任务:setTimeoutajax读取文件
二、第一种方案:时间分片
v8引擎执行js代码速度很快,但是渲染页面的时间比较漫长,如果将十万条数据直接给到渲染引擎,页面会出现白屏或卡顿,时间分片就是将一个大的任务分为一个个小的任务,可以使用定时器一次加载20条,这样一直加载结束
setTimeout实现:
init() {
const ul = document.getElementById('container')
const total = 998 // 总数据条数
const pageSize = 20 // 每次渲染条数
const index = 0 // 每条记录的索引,防止数据丢失或没有渲染到最后一条
// 两个参数:剩余需要渲染的数据条数,当前渲染的索引
function loop(curTotal, curIndex) {
const pageCount = Math.min(pageSize, curTotal)
setTimeout(() => {
for (let i = 0; i < pageCount; i++) {
const li = document.createElement('li')
li.innerText = `第${curIndex + i}条数据`
ul.appendChild(li)
}
loop(curTotal - pageCount, curIndex + pageCount)
})
}
loop(total, index)
}
如果浏览器的性能不够高,完成一次时间循环的时间大于16.7ms,那么可能在16.7ms内完成了当前渲染的20条数据,但是下一次渲染的20条数据还没有给出来,就会造成页面的白屏或卡顿,所以使用requestAnimationFrame替代setTimeout来优化
requestAnimationFrame实现:
init() {
const ul = document.getElementById('container')
const total = 998 // 总数据条数
const pageSize = 20 // 每次渲染条数
const index = 0 // 每条记录的索引,防止数据丢失或没有渲染到最后一条
// 两个参数:剩余需要渲染的数据条数,当前渲染的索引
function loop(curTotal, curIndex) {
const pageCount = Math.min(pageSize, curTotal)
requestAnimationFrame(() => {
const fragment = document.createDocumentFragment()
for (let i = 0; i < pageCount; i++) {
const li = document.createElement('li')
li.innerText = `第${curIndex + i}条数据`
fragment.appendChild(li)
}
ul.appendChild(fragment)
loop(curTotal - pageCount, curIndex + pageCount)
})
}
loop(total, index)
}
三、第二种方案:虚拟列表
app.vue
<VirtualList />
VirtualList.vue
<template>
<div ref="listRef" class="infinite-list-container" @scroll="handleScroll">
<div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>
<ul class="infinite-list" :style="{ transform: getTransform }">
<li
v-for="item in visibleData"
:key="item.id"
:style="{ height: itemHeight + 'px', lineHeight: itemHeight + 'px' }"
>
{{ item.value }}
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'VirtualList',
data() {
return {
listData: [],
itemHeight: 50,
state: {
screenHeight: 0, // 可视区域的高度
startOffset: 0, // 偏移量
start: 0, // 起始数据下标
end: 0 // 结束数据下标
}
}
},
computed: {
// 可视区域显示的数据条数
visibleCount() {
return this.state.screenHeight / this.itemHeight
},
// 可视区域显示的真实数据
visibleData() {
const { listData, state } = this
return listData.slice(state.start, Math.min(listData.length, state.end))
},
// 当前列表总高度
listHeight() {
return this.listData.length * this.itemHeight
},
// list跟着父容器移动了,现在列表要移回来
getTransform() {
return `translateY(${this.state.startOffset}px)`
}
},
mounted() {
for (let i = 0; i < 100000; i++) {
this.listData.push({ id: i, value: i })
}
this.state.screenHeight = this.$refs.listRef.clientHeight
this.state.end = this.state.start + this.visibleCount
},
methods: {
handleScroll() {
const { itemHeight, visibleCount } = this
const { scrollTop } = this.$refs.listRef
this.state.start = Math.floor(scrollTop / itemHeight)
this.state.end = this.state.start + visibleCount
this.state.startOffset = scrollTop - (scrollTop % itemHeight)
console.log('this.state.startOffset',this.state.startOffset)
}
}
}
</script>
<style lang="less" scoped>
.infinite-list-container {
height: 100%;
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch; /* 启用触摸滚动 */
.infinite-list-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
ul.infinite-list {
position: absolute;
left: 0;
top: 0;
right: 0;
text-align: center;
li {
list-style: none;
}
}
}
</style>
核心思想:
- 不管总数据有多少条,在初始化和滚动条滚动时,当前可视区域内只会渲染固定数量的数据,这样浏览器是不停地创建和销毁固定数量的DOM节点,不会造成性能问题
- ul li结构就是实际渲染的DOM,li依赖的数据就是visibleData,visibleData根据start和end在总数据listData中截取
- 如果只是渲染ul li结构,那么页面上没有显示出来滚动条啊?所以还需要一个div,它的高度是实际数据的高度,但是它不能显示在页面上,只是为了撑开父容器,把滚动条显示出来,可以用z-index: -1将它置于背景层
- 滚动条进行滚动时,做2件事,1是更新start和end,使可视区渲染出实时的数据;2是更新startOffset,使用transform将ul li拉回来(当滚动页面时,ul li会跟着一起滚动出去,所以这里需要再移回来)
vue有许多插件可以实现,包括不定高的实现:
建议使用vue-virtual-scroller,vue-virtual-scroller-list实测1万条数据,每条数据比较大时,6千多条以后的数据渲染不出来
如果你觉得这篇文章对你有用,可以看看作者封装的库xtt-utils,里面封装了非常实用的js方法。如果你也是vue开发者,那更好了,除了常用的api,还有大量的基于element-ui组件库二次封装的使用方法和自定义指令等,帮你提升开发效率。不定期更新,欢迎交流~