我们都知道,如果一次性展示所有的数据,那么会造成页面卡顿,虚拟滚动的原理就是将数据根据滚动条的位置进行动态截取,只渲染可视区域的数据,这样浏览器的性能就会大大提升,废话不多说,我们开始。
首先,我们先模拟 500 条数据
const data = new Array(500).fill(0).map((_, i) => i); // 模拟真实数
然后准备以下几个容器:
<template>
<div class="view-container">
<div class="content-container"></div>
<div class="item-container">
<div class="item"></div>
</div>
</div>
</template>
- view-container是展示数据的可视区域,即可滚动的区域
- content-container是用来撑起滚动条的区域,它的高度是实际的数据长度乘以每条数据的高度,它的作用只是用来撑起滚动条
- item-container是实际渲染数据的区域
- item则是具体渲染的数据
view-container固定定位并居中,overflow-y设置为scroll;
content-container先给它一个1000px的高度;
item-container绝对定位,top和left都设为 0;
每条数据item给他一个20px的高度;
先把 500 条数据都渲染上去看看效果:
.view-container {
height: 400px;
width: 200px;
border: 1px solid red;
overflow-y: scroll;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.content-container {
height: 1000px;
}
.item-container {
position: absolute;
top: 0;
left: 0;
}
.item {
height: 20px;
}
这里我们把高度都写死了,元素的高度是实现虚拟滚动需要用到的变量,因此肯定不能写死,我们可以用动态绑定style来给元素加上高度:
首先定义可视高度和每一条数据的高度:
const viewHeight = ref(400); // 可视容器高度
const itemHeight = ref(20); // 每一项的高度
用动态绑定样式的方式给元素加上高度:
<div class="view-container" :style="{ height: viewHeight + 'px' }">
<div
class="content-container"
:style="{
height: itemHeight * data.length + 'px',
}"
></div>
<div class="item-container">
<div
class="item"
:style="{
height: itemHeight + 'px',
}"
></div>
</div>
</div>
content-container 使用每条数据的高度乘以数据总长度来得到实际高度。
然后我们定义一个数组来动态存放需要展示的数据,初始展示前 20 条:
const showData = ref<number[]>([]); // 显示的数据
showData.value = data.slice(0, 20); // 初始展示的数据 (前20个)
showData里的数据才是我们要在item遍历渲染的数据:
<div
class="item"
:style="{
height: itemHeight + 'px',
}"
v-for="(item, index) in showData"
:key="index"
>
{{ item }}
</div>
接下来我们就可以给view-container添加滚动事件来动态改变要展示的数据,具体思路就是:
根据滚动的高度除以每一条数据的高度得到起始索引
起始索引加上容器可以展示的条数得到结束索引
根据起始结束索引截取数据
具体代码如下:
const scrollTop = ref(0); // 初始滚动距离
// 滚动事件
const handleScroll = (e: Event) => {
// 获取滚动距离
scrollTop.value = (e.target as HTMLElement).scrollTop;
// 初始索引 = 滚动距离 / 每一项的高度
const startIndex = Math.round(scrollTop.value / itemHeight.value);
// 结束索引 = 初始索引 + 容器高度 / 每一项的高度
const endIndex = startIndex + viewHeight.value / itemHeight.value;
// 根据初始索引和结束索引,截取数据
showData.value = data.slice(startIndex, endIndex);
console.log(showData.value);
};
我们不要让它滚动,可以通过调整它的 translateY 的值来实现,使其永远向下偏移滚动条的高度:
<div
class="item-container"
:style="{
transform: 'translateY(' + scrollTop + 'px)',
}"
>
<div
class="item"
:style="{
height: itemHeight + 'px',
}"
v-for="(item, index) in showData"
:key="index"
>
{{ item }}
</div>
</div>
文章到此就结束了。这只是一个简单的实现,还有很多可以优化的地方,例如滚动太快出现白屏的现象
原生js完整demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Virtual Scrolling Example</title>
<style>
#container {
width: 300px;
height: 400px;
overflow-y: auto;
position: relative;
}
.item {
height: 50px;
line-height: 50px;
text-align: center;
border-bottom: 1px solid #ccc;
}
</style>
</head>
<body>
<div id="container"></div>
<script>
// 模拟的数据源
const items = new Array(500).fill(0).map((_, x) => x);
const container = document.getElementById('container');
const itemHeight = 50; // 每个项目的高度
const bufferSize = 3; // 缓冲区大小,即在可视区域外的额外渲染数量
let renderedItems = new Set(); // 已渲染的项目集合
// 渲染函数
function render(startIndex, endIndex) {
for (let i = startIndex; i <= endIndex; i++) {
if (!renderedItems.has(i)) {
const item = document.createElement('div');
item.className = 'item';
item.textContent = `Item ${i}`;
container.appendChild(item);
renderedItems.add(i);
}
}
}
// 计算可视区域的起始和结束索引
function calculateVisibleRange() {
const scrollTop = container.scrollTop;
const visibleStartIndex = Math.floor(scrollTop / itemHeight) - bufferSize;
const visibleEndIndex = Math.min(items.length - 1, Math.floor((scrollTop + container.clientHeight) / itemHeight) + bufferSize);
return { visibleStartIndex, visibleEndIndex };
}
// 滚动事件处理
container.addEventListener('scroll', function() {
const { visibleStartIndex, visibleEndIndex } = calculateVisibleRange();
render(visibleStartIndex, visibleEndIndex);
});
// 初始渲染
const { visibleStartIndex, visibleEndIndex } = calculateVisibleRange();
render(visibleStartIndex, visibleEndIndex);
</script>
</body>
</html>
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。