最近做的一个需求,当列表大概有5万条数据,又不让做成分页,如果页面直接渲染5万条数据,在一些低配电脑上可能会照成页面卡死,基于这个需求,研究分析前端渲染卡顿原因,准备手写一个虚拟列表。
1.实现思路
- 列表中固定只显示少量的数据,比如60条
- 在列表滚动的时候不断的去插入删除dom
- startIndex、endIndex,不断的改变这个值来获取最新的显示列表
- paddingTop、paddingBottom撑开容器的滚动区域
首先看一下当直接插入10万条列表时,页面的性能。
2.方式对比
情况一
-
直接渲染出5万条数据
可以看到火焰图中已经有了红色的部分了,dom渲染也耗时也有5s多
-
优化前效果
情况二
-
采用虚拟列表加载方式
假设有一个容器,高度为600px,列表项每个高度为30px,那么根据列表的length我们就可以计算出滚动容器的总高度,也可以知道显示60条数据的高度,我们此时可以给容器加一个paddingBottom,来撑开容器,来模拟页面应该滚动的高度
this.paddingBottom = this.allHeight - this.scrollList.length * 30
容器同时还需要paddingTop用做当容器滚动顶部数据移除后撑起scrollTop
最后我们需要监听容器的滚动事件来不断的修改paddingTop、paddingBottom、startIndex、endIndex。
调整为虚拟列表之后,可以看到加载渲染在1秒左右!
-
优化后效果
3.实现代码
<!doctype html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <style> .container { width: 500px; height: 600px; overflow: auto; border: 1px solid; margin: 100px auto; } .item { height: 29px; line-height: 30px; border-bottom: 1px solid #aaa; padding-left: 20px; display: flex; text-align: center; } .item span{ flex: 1; } </style> </head> <body> <div id="app"> <button @click="add">添加数据</button> <div class="container" ref="container"> <div class="scroll-wrapper" :style="style"> <div v-for="(item, index) in scrollList" :key="index" class="item"> <span>{{item.name}}</span> <span>{{item.price}}</span> <span>{{item.category}}</span> </div> </div> </div> </div> <script src="./js/vue.js"></script> <script> new Vue({ el: '#app', data: { list: [ {name:'名称',price:'单价',category:'类型'} ], startIndex: 0, endIndex: 60, paddingTop: 0, paddingBottom: 0, allHeight: 0 }, computed: { scrollList() { return this.list.slice(this.startIndex, this.endIndex) }, style() { return { paddingTop: this.paddingTop + 'px', paddingBottom: this.paddingBottom + 'px' } } }, watch: { list(val) { const valLen = val.length this.allHeight = valLen * 30 this.paddingBottom = this.allHeight - this.scrollList.length * 30 } }, mounted() { const container = this.$refs.container container.addEventListener('scroll', () => { const top = container.scrollTop this.startIndex = Math.floor(top / 30) this.endIndex = this.startIndex + 60 this.paddingTop = top if (this.endIndex >= this.list.length - 1) { this.paddingBottom = 0 return } this.paddingBottom = this.allHeight - 600 - top }) }, methods: { add() { let arr = new Array(100000).fill(0) arr = arr.map((item, index) => { return { name: "名称_" + index, price: Math.ceil(Math.random()*10)+'元', category: Math.random() > 0.5 ? '蔬菜' : '水果' } }) this.list = [ ...this.list, ...arr ] } } }) </script> </body> </html>
总结:由于不断操作dom节点,会导致渲染慢,而且滚动起来非常卡顿,通过虚拟列表的方式,大大地提高了渲染效率和滚动流畅度!