前言
一个简单的情景模拟(千万别被带入):
A: 假设现在有 10 万条数据,你作为前端该怎么优化这种大数据的列表?
B: 针对大数据列表一般不会依次性加载,会采用上拉加载、分页加载等方式实现优化.
A: 那假如加载到最后一条数据的时候,页面上只是列表部分的数据就至少对应 10 万个 dom 节点,你觉得一个页面渲染至少 10 万个 dom 节点的性能如何?
A: 如果这样的列表有 n 个呢?还有没有别的优化方式?
B: 要不我把自己优化一下 ......
其实解决上述问题就可以使用 虚拟滚动 来实现优化,相信大家对这个词都不陌生,但由于这个名词比较短(又是虚拟
,又是滚动
)导致很多人觉得这是非常高大上、难以理解的内容,但其实恰恰相反。
本文的目的就是帮助你以 最简单 的方式 理解虚拟滚动,甚至实现 虚拟滚动.
虚拟滚动(Virtual Scrolling)
核心优化的方式:
限定文档中渲染列表数量,即固定渲染的 DOM
数,通过动态切换数据内容实现视图的更新,并保证文档中真实 DOM
的数量不随着数据量增大而增大.
理解虚拟滚动
其实要理解 虚拟滚动 这个词很简单,按照 虚拟 和 滚动 两部分来理解就很简单了,下面就一一拆解。
虚拟
通常在页面列表中,要渲染的 列表数量
和真实在文档中存在的 DOM 节点数
是 1 : 1
的。每个列表项都拥有相同的高度(假设是 30px
),这个列表容器中需要完全渲染的列表数(假设是 100 条
)和在页面中的高度是一致的,即此时的高度就为 100 * 30 = 3000
,对应列表的 DOM
数量为 100
,如:
对于 虚拟滚动 来讲,虚拟
的意思是指实际要渲染完整列表对应的高度是通过 虚拟计算
的,并不是指文档中存在对应的 DOM 节点数
。
上面的栗子对应到虚拟滚动来讲,就意味着实际渲染完整列表对应的高度就仍为 100 * 30 = 3000
,但实际渲染数就变为 10 条
,关系图大致如下:
滚动
所谓 滚动 就很好理解了,因为列表可视区通常会限制一定的高度,即 列表可视区高度
,那么此时只要 虚拟列表高度
值大于 列表可视区高度
时,就会产生滚动条即可发生滚动操作。
值得注意的是,在发生滚动时需要对 实际渲染的列表
进行一些处理,否则会出现 实际渲染的列表
和 虚拟列表区
脱离的情况,比如:
关键点就是实现在发生 滚动 操作时,保证 实际渲染的列表
一直存在 列表可视区 中,并且动态切换需要渲染的列表数据。
实现虚拟滚动
核心步骤
- 设置列表可视区的高度
containerHeight
- 设置单个列表项的高度
listItemHeight
- 计算渲染完整列表需要的高度
virtualHeight
,即virtualHeight = listItemHeight * data.length
- 设置真实渲染数据的起始索引
startIndex、endIndex
,用于从列表数据data
中获取对应的数据内容 - 注册/监听滚动事件
onScroll
- 获取当前实际滚动距离
eleScrollTop
- 将
eleScrollTop
作为translateY
的值,即实际渲染列表元素
平移的数值,保证实际渲染列表元素
一直存在可视区中 - 根据实际的滚动距离
eleScrollTop
,动态计算列表新的起始索引startIndex、endIndex
- 获取当前实际滚动距离
效果预览
具体实现细节都在如下的代码中,可结合其中的注释阅读:
// App.vue
<script>
const list = (num = 10)=> {
const data = [];
for (let i = 0; i < num; i++) {
data.push({
id: i+1,
name: `第 ${i+1} 条列表`
});
}
return data;
}
</script>
<template>
<VirtualScroll :data="list(100)" />
</template>
// VirtualScroll.vue
<template>
<!-- 虚拟滚动内容 -->
<div
class="virtual-scroller"
@scroll="onScroll"
:style="{ height: containerHeight + 'px' }"
>
<!-- 实际渲染的列表内容 -->
<ul
class="real-list-content"
:style="{ transform: `translateY(${translateY}px)` }"
>
<li
v-for="item in visibleList"
:key="item.id"
:style="{
height: `${listItemHeight}px`,
'line-height': `${listItemHeight}px`,
}"
>
<div>{{ item.name }}</div>
</li>
</ul>
<!-- 虚拟列表元素 -->
<div class="virtual-height" :style="{ height: virtualHeight + 'px' }">
~ 数据加载完毕 ~
</div>
</div>
</template>
<script>
export default {
name: "vue-virtual-scroll",
};
</script>
<script setup language="ts">
import { computed, ref } from "vue";
const props = defineProps({
data: {
type: Array,
default: [],
},
startIndex: {
type: Number,
default: 0,
},
endIndex: {
type: Number,
default: 10,
},
listItemHeight: {
type: Number,
default: 60,
},
containerHeight: {
type: Number,
default: 500,
},
});
let { data, listItemHeight } = props;
const translateY = ref(0); // 平移距离
const startIndex = ref(props.startIndex); // 开始索引
const endIndex = ref(props.endIndex); // 结束索引
// 实际渲染的数据
const visibleList = computed(() => {
return data.slice(startIndex.value, endIndex.value);
});
// 虚拟滚动的高度
const virtualHeight = computed(() => {
return (data.length - visibleList.value.length) * listItemHeight + listItemHeight;
});
// 滚动事件
const onScroll = (e) => {
const eleScrollTop = e.target.scrollTop;
// 保证实际渲染列表一直停留在可视区
translateY.value = eleScrollTop;
// 根据实际的滚动距离,动态计算列表开始索引
startIndex.value = Math.floor(eleScrollTop / listItemHeight);
// 基于开始索引
endIndex.value = startIndex.value + 10;
};
</script>
<style scoped>
.virtual-scroller {
border: solid 1px #eee;
margin-top: 10px;
height: 600px;
overflow: auto;
}
.virtual-height {
background: red;
display: flex;
align-items: end;
justify-content: center;
color: #fff;
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
li {
outline: solid 1px #fff;
background-color: #000;
color: #fff;
}
</style>
最后
其实 虚拟滚动 并不难理解,就像 CSS 中的 BFC、JavaScript 中的闭包 等概念一样,最初了解时你很难给它一个定义,但是实际上下功夫去了解它,其实也就那么一回事。
以上的实现方式是极简的方式,没有做任何的优化、没有考虑额外的场景,因为本文的目的还是想通过最简单的实现去解释虚拟滚动到底是怎么一回事,因此不必过于纠结,当然基于 vue
相关库的实现如 vue-virtual-scroller 可自行了解。