🪛 关于虚拟滚动
0️⃣ Begin
秉承着用不到就不去了解的初衷(用不到也要看的 😡
于是还是碰上了
在编写一个图标选择组件,图标库使用的 Fontawesome
原本想使用遍历产生 dom ,不过图标太多了,依照此方案或许会有性能问题
然后恰巧最近在某个B站直播弹幕采集站上看到了其使用虚拟滚动实现的弹幕列表
(顺带一提,B站的直播间弹幕也有使用虚拟滚动,不过其实现的思路似乎并不一致
对于当下热门的前端框架,Vue或者React,实际上也都有现成的轮子可以使用
1️⃣ 所以,我选择手搓 🤭
(这个例子中,滚动会修 `translateY` 的值,这是为了让视口保持在同一位置,不过似乎直接使用 `sticky top-0` 就好了 🤔)
虚拟滚动本质上都是限制 `DOM元素` 的渲染数量,并依靠滚动来不断刷新可视区域的内容
我所写的Demo结构是这样的 👇
- 绿色部分表示外层容器
- 紫色部分为缓冲区,需要手动计算高度以撑开外层容器,使容器出现滚动条
- 蓝色部分为视口,高度与外层容器一致,用于渲染图标(内容)
前置条件是容器以及内容的高度需要固定,比如我这边给了外层容器的高度为 ' 300px ' ,内容元素 ' 75px ',然后每次渲染 20 个图标
1️⃣ Source Code
<template>
<div class="fixed flex justify-center items-center bg-black bg-opacity-10 top-0 bottom-0 left-0 right-0 z-[99]">
<div class="w-[600px] bg-white rounded-md flex flex-col p-[20px] gap-[10px]">
<div class="flex items-center">
<span class="text-gray-600 text-[14px]">图标分类</span>
<el-radio-group v-model="currentIndex" class="ml-4" @change="indexChange">
<el-radio :label="index" v-for="(item, index) in icons" :key="index">{{
item.prefix
}}</el-radio>
</el-radio-group>
</div>
<el-input v-model="iconName" placeholder="请输入"></el-input>
<div class="overflow-y-auto border-1 border-solid border-gray-300 relative" :style="{ height: `${containerH}px` }"
@scroll="scrollHanlde" id="icon-viewer">
<div :style="{ height: `${virtualDomHeight}px`, paddingBottom: `${itemH}px` }">
<div class="sticky top-0 left-0 right-0" :style="{ height: `${containerH}px` }">
<div :class="[
`grid grid-cols-5 h-full relative`
]" :style="{ top: `${topV}px` }">
<div class="flex flex-col justify-center items-center gap-[10px]" :style="{ height: `${itemH}px` }"
v-for="(item, index) in virtualList" :key="index">
<font-awesome-icon :icon="[icons[iconIndex].prefix, item]" class="text-blue-400 text-18px" />
<span class="text-[12px] max-w-[80px] overflow-hidden text-gray-500 whitespace-nowrap text-ellipsis">{{
item
}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeMount } from "vue";
import { fas } from "@fortawesome/free-solid-svg-icons";
import { fab } from "@fortawesome/free-brands-svg-icons";
import { far } from "@fortawesome/free-regular-svg-icons";
import type { IconPrefix } from "@fortawesome/fontawesome-common-types";
type Icon = {
prefix?: IconPrefix;
icons?: string[];
};
let iconName = ref<string>("");
// 容器高度
let containerH: number = 300,
// 图标item高度
itemH: number = 75,
// 每行元素数量
rowNum: number = 5;
let icons = ref<Icon[]>([]);
let currentIndex = ref<number>(0), iconIndex = ref<number>(0);
let virtualDomHeight = ref<number>(0);
let virtualList = ref<string[]>([]);
let containerDom: Element | null = null;
let topV = ref<number>(0)
onMounted(() => {
containerDom = document.querySelector("#icon-viewer");
showIconHandle(0);
});
const scrollHanlde = (e: any) => {
showIconHandle(e.target.scrollTop)
}
let lastBegin = 0
const showIconHandle = (y: number) => {
virtualDomHeight.value = (icons.value[iconIndex.value]?.icons?.length || 0) * 15;
let start = Math.floor(y / itemH) * rowNum;
let end = Math.floor((y + containerH) / itemH) * rowNum + 5;
lastBegin = start
virtualList.value = icons.value[iconIndex.value]?.icons?.map((item, index) => item).slice(start, end) || []
if (lastBegin === start) {
topV.value = -y % itemH
} else {
topV.value = 0
}
};
const indexChange = (e: any) => {
virtualList.value = []
iconIndex.value = e;
showIconHandle(0);
containerDom?.scrollTo({
top: 0,
});
};
onBeforeMount(() => {
const allIcons = {
...fas,
...fab,
...far,
};
Object.keys(allIcons).forEach((iconName) => {
const icon = allIcons[iconName];
let iconType: Icon = icons.value.filter((item) => item.prefix === icon.prefix)[0];
if (!iconType) {
iconType = {
prefix: icon.prefix,
icons: [],
};
iconType.icons?.push(icon.iconName);
icons.value.push(iconType);
} else {
iconType.icons?.push(icon.iconName);
}
});
});
</script>
2️⃣ End
这里光依靠对外层滚动条的监听来渲染图标并没有滚动效果,所以加上了 `topV`,这样可以让它看起来更像是在滚动,于是我就有点诟病这个点 🤣
然后也和 ChatGPT 唠了唠,其给出的方案十分传统和简单
` 监听滚动条,触底时添加元素 `
这样可以避免一次性的渲染,但是也会随着滚动而导致元素越来越多
上方的图标选择器其实可以用这种方式来实现,因为其大多是在搜索后进行图标的选择,并不会频繁进行滚动
另外有想到几种其它的方案,不过暂时还没有实践,有机会再写捏 😴
石上优
牛批
xiamo
@石上优 优哥不要捧杀我 😭