使用场景

在日常开发中,当服务端返回一个列表时,如果列表长度比较短的话可以直接将列表渲染到页面当中,但是对于长列表来说,将其渲染到页面中将会花费很长时间,以下面这段代码为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<ul class="list">
<li class="list-item" v-for="item in data.list" :key="item">{{ item }}</li>
</ul>
</template>

<script setup lang="ts">
import { reactive, onMounted } from "vue";

const data = reactive({
list: [],
});

onMounted(() => {
const tmp = [];
const startTime = Date.now();

for (let i = 0; i < 10000; i++) {
tmp.push(i);
}

data.list = tmp;

// js执行时间0.001s
console.log("js执行时间", (Date.now() - startTime) / 1000);

setTimeout(() => {
// 总执行时间1.588s
console.log("js执行时间 + 渲染时间", (Date.now() - startTime) / 1000);
}, 0);
});
</script>

在以上代码中,我们将 10000 条数据一次性渲染到了页面当中,总用时为1.588s,其中渲染时间占了1.587s。可以得出在渲染列表时,大部分时间都花在了页面渲染阶段。究其原因是因为渲染的dom节点太多,因此可以通过减少渲染dom节点数量来降低页面渲染阶段的用时。虚拟列表正是解决这个问题的有效方法。

什么是虚拟列表

虚拟列表是指仅仅渲染可视区域内的列表项,超出的不渲染,这样的话每次渲染的节点数将极大的减少,相应的渲染时间也会大幅降低。

虚拟列表的渲染主要分为两部分。首先是首次渲染,直接取列表的前n条数据即可。其次是滚动渲染,监听列表容器的滚动事件,并实时计算当前需要渲染的子列表[startIndex, endIndex]

实现

首先实现虚拟列表Dom结构。结构如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<template>
<!-- 虚拟列表容器 -->
<div class="virtual-list" ref="virtualListContainer">
<!-- 占位dom,将其高度设置为itemHeight * list.length,用来撑起容器盒子实现滚动效果 -->
<div
class="virtual-list-placeholder"
:style="{ height: `${itemHeight * list.length}` }"
></div>
<!-- 列表项盒子 -->
<div
class="virtual-list-box"
:style="{ transform: `translate3d(0,${offsetY}px, 0)` }"
>
<!-- 列表项 -->
<div class="virtual-list-box-item"></div>
</div>
</div>
</template>

<style lang="less">
* {
box-sizing: border-box;
}

.virtual-list {
overflow: auto;
position: relative;
max-height: 400px;
border: 1px solid #ccc;

&-box {
position: absolute;
top: 0;
left: 0;
width: 100%;
/* 需要取消鼠标事件,否则无法进行滚动 */
pointer-events: none;

&-item {
height: 50px;
text-align: center;
line-height: 50px;
}
}
}
</style>

注意

  1. virtual-list-box列表盒子是相对virtual-list容器盒子进行定位的,当容器盒子滚动时列表盒子也会跟着滚动,因此需要给列表盒子设置transform: translate3d(0,offsetY, 0)属性将其移动到视口区域。

计算前进行以下约定

  1. 容器的可视高度为visibleHeight
  2. 列表项的高度为itemHeight
  3. 容器可视区域内列表项的个数为virtualListLength
  4. 容器滚动的距离为scrollTop

需要计算的值包括:

  1. 容器滚动的距离scrollTop = virtualListContainer.scrollTop
  2. 计算列表开始位置startIndex = Math.floor(scrollTop / itemHeight)
  3. 计算列表结束位置:endIndex = startIndex + virtualListLength
  4. **计算列表盒子的偏移量(translateY 的值)**:offsetY = Math.floor(scrollTop / itemHeight) * itemHeight

点我查看完整代码

参考