vue针对滚动元素内部大量元素,但只有部分元素可见,对dom懒渲染,节省内存的优化
我们开发过程中经常会遇到这样的需求,一个数据量很大的列表。
页面打开,加载第一页的数据,每次往下滚屏到接近底部,会加载下一屏,这样也是保证获取数据的http请求量是按需加载的。图片懒加载,也能节省流量的资源。但是,随着我们一直加载,产生的dom会越来越多。内存占用也会越来越高
![image.png](https://s2.loli.net/2022/08/06/8Ox4EAcNKjsHRT5.png)
还有我们常见的列表,很多情况下我们通过分页来加载,但是有些情况下,比如不能使用分页,这时候如果有千上万的数据,就会产生大量的虚拟dom和真实dom,大量占用内存
如果说上面的列表我们还能通过分页
解决,那么Select选择框
,如果有成千上万的选项,即使我们使用了滚动加载,滚动到最后也还是会产生大量的dom,我本地测试了1万条选项的下拉框,卡顿显现非常明显,点击后需要2s才能展开选项,并且我的笔记本风扇已经开始呼呼排风了。
![image.png](https://s2.loli.net/2022/08/06/Vj8yd4buhn1lISD.png)
![image.png](https://s2.loli.net/2022/08/06/3tEpHU4nLmjXAdu.png)
这三类问题都有一个相似点,大量的并列的dom,但是这些dom都在一个可滚动的容器中,无论怎么滚动,只有一部分是可见的,
![image.png](https://s2.loli.net/2022/08/06/fneHT2LZ7i4Qqaj.png)
所以如果只有可见的数据才去创建虚拟dom,就会大量节省内存,并加快渲染速度。
我们以列表来作为例子,展示解决方案
![image.png](https://s2.loli.net/2022/08/06/Za4cJjOhS1MzuqC.png)
我们做一点改动,通过代码生成一个10000条的list
<template>
<div class="list" style="height: 300px;overflow: scroll">
<div v-for="(item,index) in list" :key="index" class="item">
<div v-html="item.id"></div>
<div v-html="item.name"></div>
<div v-html="item.createTime"></div>
<div>
<Button>修改</Button>
<Button>删除</Button>
</div>
</div>
</div>
</template>
<script>
export default {
created() {
for (let i = 0; i < 10000; i++) {
this.list.push({
id: 1,
name: 'name' + i,
createTime: '2018-09-09'
})
}
},
data() {
return {
list: []
}
}
}
</script>
根据这个想法设计的解决方案,思路有几个关键点。
1、假滚动事件:拖动滚动滑块,并不会影响左侧的真实滚动,只是记录滚动的位置。
2、根据滚动的位置,计算出对应的数据区间段,比如应该取340-350这10条。
3、根据滚动位置,和数据区间,把这几条数据控制绝对定位,来模拟滚动效果。
第一步:假滚动事件
<template>
<div class="list" style="height: 300px;overflow: scroll">
<div :style="{height:list.length*40+'px'}"></div>
</div>
</template>
我们的每一行高度是40,那么我们创建一个高度时list.length*40的空元素,那么这个空元素的高度,跟展示所有的list的高度一样,也就是说右侧的滚动条效果跟原来的效果是一样的。
![image.png](https://s2.loli.net/2022/08/06/fFDQ7klH5mrAgO4.png)
第二步,根据滚动的位置,计算出对应的数据区间段
我们需要计算开始index和长度,然后截取list数据的一部分
<template>
<div class="list" style="height: 300px;overflow: scroll" ref="scrollDom" @scroll="scroll">
<div :style="{height:list.length*40+'px'}"></div>
</div>
</template>
<script>
export default {
created() {
for (let i = 0; i < 10000; i++) {
this.list.push({
id: 1,
name: 'name' + i,
createTime: '2018-09-09'
})
}
},
computed: {
limitCount() {
return 300 / 40;
}
},
data() {
return {
startIndex:0,
list: []
}
},
methods: {
scroll() {
// 根据滚动的距离,估算出这个滚动位置对应的数组序列,例如滚动100px,每条40px,对应第3条
let scrollTop = this.$refs.scrollDom.scrollTop;
this.startIndex = Math.floor(scrollTop / 40);
console.log(this.startIndex);
}
}
}
</script>
![1245001-20190712193551039-908892724.gif](https://s2.loli.net/2022/08/06/hjoAmyaqzB1ZWe5.gif)
我们根据开始和结束需要,生成一个splitData,然后再把新生成的dom通过绝对位置调整。
![image.png](https://s2.loli.net/2022/08/06/G5OeaKsuqHQBm4T.png)
完成的效果
![1245001-20190712193622586-717652041.gif](https://s2.loli.net/2022/08/06/5HgW8vMulJkfoir.gif)
我们可以看到,无论怎么滚动,效果跟全部加载是一样的,但是通过右侧的控制台发现,dom数量只有固定的这几个,但是每个dom内部的值在不停的修改。
完整的代码
<!doctype html>
<html lang="en">
<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>
</head>
<body>
<div id="app">
<div class="list" style="height: 300px;overflow: scroll" ref="scrollDom" @scroll="scroll">
<div :style="{height:list.length*40+'px'}"></div>
<div style="position:absolute;width: 100%" :style="{top:startIndex*40+'px'}">
<div v-for="(item,index) in splitData" :key="index" class="item">
<div v-html="item.id"></div>
<div v-html="item.name"></div>
<div v-html="item.createTime"></div>
<div>
<Button>修改</Button>
<Button>删除</Button>
</div>
</div>
</div>
</div>
</div>
<script src="https://lib.baomitu.com/vue/2.6.0/vue.min.js"></script>
<script>
new Vue({
el: '#app',
data: {
startIndex: 0,
list: []
},
created() {
for (let i = 0; i < 10000; i++) {
this.list.push({
id: 1,
name: 'name' + i,
createTime: '2018-09-09'
})
}
},
computed: {
limitCount() {
return 300 / 40 + 2;
},
splitData() {
return this.list.slice(this.startIndex, this.startIndex + this.limitCount)
}
},
methods: {
scroll() {
// 根据滚动的距离,估算出这个滚动位置对应的数组序列,例如滚动100px,每条40px,对应第3条
let scrollTop = this.$refs.scrollDom.scrollTop;
this.startIndex = Math.floor(scrollTop / 40);
console.log(this.startIndex + this.limitCount);
}
}
})
</script>
<style>
.list {
border: solid 1px #5f5f5f;
background-color: white;
margin: 100px 0;
padding: 5px;
width: 500px;
position: relative;
}
.item {
display: flex;
height: 40px;
}
.item > * {
flex-grow: 1;
border: solid 1px #9e9e9e;
padding: 3px;
}
</style>
</body>
</html>
借助插件
这个方法,也可以灵活的运用到所有类似的情景,解决了大量dom的内存优化问题