简直不要太简单,vue3 setup 语法实现虚拟滚动列表
吃水不忘挖井人,本文受到百毒不侵 (三)」结合“康熙选秀”,给大家讲讲“虚拟列表”文章的启发。
我将原文 vue2 的写法改为了 vue3 setup 语法,并且作了全新易于理解的注释,让咋们能从另一个角度去理解虚拟滚动列表的实现。
并且将某些错误纠正,复制粘贴进编辑器运行即可出效果。
我理解的虚拟滚动列表,其实就是在屏幕上渲染那固定的几十条数据。上下滚动屏幕时再动态去计算与以往不同的几十条数据。
js
<template>
<div class="v-scroll" @scroll="doScroll" ref="scrollBox">
<div :style="blankStyle" style="height: 100%">
<div v-for="item in tempSanxins" :key="item.id" class="scroll-item">
<span>{{ item.msg }}</span>
<img :src="item.src" />
</div>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, ref } from "vue";
import { throttle } from "lodash";
// #region 模拟请求数据相关
// 所有数据
const allSanxins = ref([]);
getAllSanxin(30); // 在这请求相当于在 vue2 的 created 钩子里请求
// 模拟请求后台数据的方法。
// 不一定要一次性请求 3 万条数据,也可以是分批次请求很多次,将数据保存在一个数组中。
function getAllSanxin(count) {
// 模拟获取数据
const length = allSanxins.value.length;
for (let i = 0; i < count; i++) {
allSanxins.value.push({
id: `id: ${length + i}`,
msg: `我是嘉心糖${length + i}号`,
src: "https://freenaturestock.com/wp-content/uploads/freenaturestock-2053-768x1152.jpg",
});
}
}
// #endregion
// #region 滚动盒子容器相关
// 滚动盒子的ref
const scrollBox = ref(null);
// 可视区域的高度
const boxHeight = ref(0);
// 获取可视区域的高度
function getScrollBoxHeight() {
boxHeight.value = scrollBox.value.clientHeight;
}
onMounted(() => {
// 在 mounted 时获取可视区域的高度
getScrollBoxHeight();
// 监听屏幕变化以及旋转,都要重新获取可视区域的高度
window.onresize = getScrollBoxHeight;
window.onorientationchange = getScrollBoxHeight;
})
// #endregion
// #region 核心
// 列表每一项的高度
const itemHiehgt = ref(150);
// 可视区域可展示多少个列表项?计算公式:~~(可视化区域高度 / 列表项高度) + 2
// ~~是向下取整的运算符,等同于 Math.floor(),为什么要 +2,是因为可能最上面和最下面的元素都只展示一部分
const itemNum = computed(() => {
return ~~(boxHeight.value / itemHiehgt.value) + 2;
});
// 元素开始索引(这个很重要,很多计算属性都是根据它来计算的,可以说它是很多计算属性的因变量。)
// 可以理解为在可视区域第一项元素的索引
const startIndex = ref(0);
// 监听可视区域的滚动事件
// 公式:~~(滚动的距离 / 列表项 ),就能算出已经滚过了多少个列表项,也就能知道现在的 startIndex 是多少
// 例如我滚动条滚过了 160px,那么 index 就是 1,因为此时第一个列表项已经被滚上去了,可视区域里的第一项的索引是 1
const doScroll = throttle(function () {
const index = ~~(scrollBox.value.scrollTop / itemHiehgt.value);
if (index === startIndex.value) return;
startIndex.value = index;
// 假设后端没有一次性返回 3 万条数据,我们的数据是累加到 3 万条的。
if (startIndex.value + itemNum.value > allSanxins.value.length - 1) {
getAllSanxin(30);
}
}, 200);
// 基于 startIndex 计算。要考虑不能超过总数据的最大索引
const endIndex = computed(() => {
// 为了防止滑动过快白屏
let index = startIndex.value + itemNum.value * 2;
if (!allSanxins.value[index]) {
index = allSanxins.value.length - 1;
}
return index;
});
// 真正渲染在屏幕上的数组。
// 以 startIndex 为准,截取 startIndex 前 itemNum 条数据,startIndex 后 itemNum*2 条数据。
// 要考虑 startIndex 前 itemNum 条数据可能不存在的情况
const tempSanxins = computed(() => {
// 可视区域展示的截取数据,使用了数组的 slice 方法,不改变原数组又能截取
let index = 0;
if (startIndex.value <= itemNum.value) {
index = 0;
} else {
index = startIndex.value - itemNum.value;
}
return allSanxins.value.slice(index, endIndex.value + 1);
});
// 用padding来占位为渲染出来的数据,让滚动条看起来更真实
const blankStyle = computed(() => {
// 上下方的空白处使用 padding 来充当
let index = 0;
if (startIndex.value <= itemNum.value) {
index = 0;
} else {
index = startIndex.value - itemNum.value;
}
return {
// 上方空白的高度计算公式:(开始index * 列表项高度)
// 比如你滚过了3个列表项,那么上方空白区高度就是3 * 150 = 450,这样才能假装10000个数据的滚动状态
paddingTop: index * itemHiehgt.value + "px",
// 下方空白的高度计算公式:(总数据的个数 - 结束index - 1) * 列表项高度
// 例如现在结束index是100,那么下方空白高度就是:(10000 - 100 - 1) * 150 = 1,484,850
paddingBottom:
(allSanxins.value.length - endIndex.value - 1) * itemHiehgt.value + "px",
// 不要忘了加px哦
};
});
// #endregion
</script>
<style lang="scss" scoped>
.v-scroll {
height: 100vh;
// padding-bottom: 500px;
overflow: auto;
.scroll-item {
height: 148px;
/* width: 100%; */
border: 1px solid black;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
img {
height: 100%;
}
}
}
</style>