虚拟滚动的实现
前提
虚拟滚动是一种动态渲染页面元素的技术。当页面需要展示大量数据时(例如一万条),不能一次性将所有数据渲染到页面上,否则会导致性能问题。因此,需要采用虚拟滚动技术,假装展示了全部数据,实际上只渲染了一部分。
步骤
首先,我们的网页的 HTML 结构如下:
<div class="appbox" ref="appBox" @scroll="appBoxScroll($event)">
<div class="appbox-scroll" :style="{ height: containerHeight + 'px' }" style="position: relative;">
<div :style="{ transform: `translateY(${appBoxOffset}px)` }" style="position: absolute; width: 100%;">
<div class="boxcontent" v-for="(item, key) in showItem" :key="key">
{{ item }}
</div>
</div>
</div>
</div>
对应的 CSS 样式如下:
.appbox {
position: relative;
width: 200px;
height: 200px;
overflow: auto;
border: 1px solid #ccc;
}
.boxcontent {
height: 40px;
line-height: 40px;
border: 1px solid red;
}
逐步解释
-
<div class="appbox" ref="appBox" @scroll="appBoxScroll($event)">
:一个具有 CSS 类名为 “appbox” 的<div>
元素。通过ref
属性设置了一个引用名为 “appBox”,可以在 Vue 组件中通过this.$refs.appBox
来引用该元素。通过@scroll
事件绑定了一个滚动事件,当滚动事件触发时,会调用 Vue 组件中的appBoxScroll
方法,并将事件对象$event
作为参数传递给该方法。 -
<div class="appbox-scroll" :style="{ height: containerHeight + 'px' }" style="position: relative;">
:一个具有 CSS 类名为 “appbox-scroll” 的<div>
元素。通过动态绑定的方式设置了该元素的高度,绑定的值为containerHeight
加上 ‘px’ 单位。containerHeight
是一个在 Vue 组件中定义的响应式数据,其值会随着数据的变化而更新。 -
<div :style="{ transform:
translateY(${appBoxOffset}px)}" style="position: absolute; width: 100%;">
:该<div>
元素的样式部分包含了两个部分:-
:style="{ transform:
translateY(${appBoxOffset}px)}"
:使用动态绑定的方式设置该<div>
元素的变换样式,通过appBoxOffset
的值来设置translateY
的偏移量。appBoxOffset
是一个在 Vue 组件中定义的响应式数据,其值会随着数据的变化而更新。 -
style="position: absolute; width: 100%;"
:设置该<div>
元素的绝对定位和宽度为 100%。
-
-
<div class="boxcontent" v-for="(item, key) in showItem" :key="key">
:一个具有 CSS 类名为 “boxcontent” 的<div>
元素,根据v-for
指令生成多个。v-for
指令根据showItem
数组的内容循环生成多个<div class="boxcontent">
元素。showItem
是一个在 Vue 组件中定义的计算属性,根据条件切片处理数据后返回一个新的数组。 -
{{ item }}
:在每个<div>
元素中显示当前循环项item
的内容。
JavaScript 部分
通过使用 ref
,我们可以创建具有响应性的数据引用。使用 computed
,我们可以创建根据其他响应式数据自动计算的属性。而 nextTick
则用于在 DOM 更新后执行异步操作。
import { ref, computed, nextTick } from "vue";
下面的代码使用 computed
函数创建了一个计算属性 showItem
。计算属性是根据其他响应式数据自动计算的属性。
const showItem = computed(() => {
return [...arr.value.slice(currentIndex.value, showItemNum.value + currentIndex.value + 1)];
});
在这里,showItem
的计算函数使用了三个响应式数据:arr.value
、currentIndex.value
和 showItemNum.value
。它从 arr.value
数组中提取了一部分元素,这部分元素的起始索引是 currentIndex.value
,要提取的元素个数是 showItemNum.value
。最后,计算函数返回提取出的元素作为 showItem
的值。这样,每当 arr.value
、currentIndex.value
或 showItemNum.value
发生变化时,showItem
的值会自动重新计算。
接下来是一些初始变量的定义:
// 设置1W条模拟数据
const count = ref(10000);
let arr = ref([]);
for (let index = 0; index < count.value; index++) {
arr.value.push(index);
}
// 容器真实高度,给增加滚动条,假装渲染数据多
let containerHeight = ref(arr.value.length * 40);
// 当前状态索引
let currentIndex = ref(0);
// 应该显示的 DOM 数量
let showItemNum = ref(0);
// 容器 DOM 节点
const appBox = ref(null);
// 容器视窗高度
let appBoxHeight = ref(0);
// 容器内部元素 DOM 节点下降偏移
let appBoxOffset = ref(0);
// 老高度
let oldOffset = 0;
在回调函数中,首先通过 appBox.value.clientHeight
获取了容器的高度,并将其赋值给 appBoxHeight.value
。接下来,通过运算 Math.ceil(appBoxHeight.value / 40)
,计算出应该显示的 DOM 元素的数量,并将结果赋值给 showItemNum.value
。这里每个 DOM 元素的高度是 40 像素。通过将这两个值赋给响应式数据 appBoxHeight.value
和 showItemNum.value
,可以使它们成为计算属性的依赖项,从而触发计算属性的重新计算。
nextTick(() => {
// 获取容器高度
appBoxHeight.value = appBox.value.clientHeight;
// 运算出应该显示的 DOM 数量
showItemNum.value = Math.ceil(appBoxHeight.value / 40);
});
最后是关键的方法函数代码,其中有注释帮助理解:
const appBoxScroll = (e) => {
let tempNum = 0;
let scrollTop = e.target.scrollTop;
// 判断向下还是向上滑动
if (scrollTop < oldOffset) {
// 计算当前状态的索引
tempNum = Math.floor(scrollTop / 40);
// 当前状态的索引发生变化才触发视图层刷新
if (tempNum !== currentIndex.value && scrollTop > 40) {
currentIndex.value = tempNum;
appBoxOffset.value = scrollTop - 40;
} else if (scrollTop <= 40) {
appBoxOffset.value = 0;
currentIndex.value = 0;
}
} else {
// 计算当前状态的索引
tempNum = Math.floor(e.target.scrollTop / 40);
// 当前状态的索引发生变化才触发视图层刷新
if (tempNum !== currentIndex.value) {
currentIndex.value = tempNum;
appBoxOffset.value = scrollTop;
}
}
// 记录老高度
oldOffset = scrollTop;
};
以上就是实现虚拟滚动的代码。在 Vue 组件中,可以将 HTML、CSS 和 JavaScript 部分整合在一起使用。通过这些代码,可以实现虚拟滚动效果,优化大量数据的渲染性能。
效果:
全部代码
<template>
<div class="appbox" ref="appBox" @scroll="appBoxScroll($event)">
<div class="appbox-scroll" :style="{ height: containerHeight + 'px' }" style="position: relative;">
<div :style="{ transform: `translateY(${appBoxOffset}px)` }" style="position: absolute; width: 100%;">
<div class="boxcontent" v-for="(item, key) in showItem" :key="key">
{{ item }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, nextTick } from "vue";
const showItem = computed(() => {
return [...arr.value.slice(currentIndex.value, showItemNum.value + currentIndex.value + 1)];
});
//设置1W条模拟数据
const count = ref(10000);
let arr = ref([]);
for (let index = 0; index < count.value; index++) {
arr.value.push(index);
}
//容器真实高度,给增加滚动条,假装渲染数据多
let containerHeight = ref(arr.value.length * 40);
// 当前状态索引
let currentIndex = ref(0);
// 应该显示的 DOM 数量
let showItemNum = ref(0);
// 容器dom节点
const appBox = ref(null);
// 容器视窗高度
let appBoxHeight = ref(0);
// 容器内部元素dom节点下降偏移
let appBoxOffset = ref(0);
// 老高度
let oldOffset = 0;
nextTick(() => {
//获取容器高度
appBoxHeight.value = appBox.value.clientHeight;
//运算出应该显示的 DOM 数量
showItemNum.value = Math.ceil(appBoxHeight.value / 40);
});
const appBoxScroll = (e) => {
let tempNum = 0;
let scrollTop = e.target.scrollTop;
// 判断向下还是向上滑动
if (scrollTop < oldOffset) {
//计算当前状态的索引
tempNum = Math.floor(scrollTop / 40);
//当前状态的索引发生变化才触发视图层刷新
if (tempNum !== currentIndex.value && scrollTop > 40) {
currentIndex.value = tempNum;
appBoxOffset.value = scrollTop - 40;
} else if (scrollTop <= 40) {
appBoxOffset.value = 0;
currentIndex.value = 0;
}
} else {
//计算当前状态的索引
tempNum = Math.floor(e.target.scrollTop / 40);
//当前状态的索引发生变化才触发视图层刷新
if (tempNum !== currentIndex.value) {
currentIndex.value = tempNum
appBoxOffset.value = scrollTop;
}
}
// 记录老高度
oldOffset = scrollTop;
};
</script>
<style>
.appbox {
position: relative;
width: 200px;
height: 200px;
overflow: auto;
border: 1px solid #ccc;
}
.boxcontent {
height: 40px;
line-height: 40px;
border: 1px solid red;
}
</style>