前端虚拟列表实现详解
嘿,各位前端大佬们!当我们需要展示大量数据列表时,传统的列表渲染方式会面临诸多痛点,内存占用过高、页面卡顿、 响应速度变慢等等。而虚拟列表则是解决这些痛点的有效方案。
整体功能概述
这段代码主要实现了一个无限滚动加载的列表。当用户滚动列表到底部时,会自动加载更多的数据,从而解决了前端在渲染数据量比较大的页面时出现的诸多问题。其核心原理是只渲染当前可见区域以及其附近的少量数据,通过监听滚动事件动态计算需要显示的数据,同时在列表上下添加空白填充,让滚动更加流畅,避免一次性渲染大量数据导致的性能问题。
代码结构剖析
1. 引入必要的依赖
<script setup>
import axios from "axios";
import { ref, onMounted, computed } from "vue";
这里引入了 axios 用于发送 HTTP 请求,获取数据。同时引入了 Vue 3 的一些响应式 API,如 ref、onMounted 和 computed。ref 用于创建响应式数据,onMounted 用于在组件挂载后执行代码,computed 用于创建计算属性。
2. 定义响应式数据
// 总数据
const allList = ref([]);
// 最大容积
const contentSize = ref(0);
// 容器
const scroll = ref(null);
// 单个数据高度
const oneHeight = ref(100);
// 容器第一条数据索引
const startIndex = ref(0);
这些变量就像是我们的“小助手”,分别存储了所有数据、容器的最大容积、滚动容器的引用、单个数据项的高度以及当前显示数据的起始索引。
3. 获取数据
const getListData = async (num = 10) => {
try {
const res = await axios.get("http:://localhost:9527/mock", {
params: { num },
});
allList.value = [...allList.value, ...res.data.list];
} catch (error) {
console.error("获取数据失败:", error);
}
};
getListData 函数用于从服务器获取数据。使用 axios 发送 GET 请求,将获取到的数据追加到 allList 中。这里的 URL 是本地模拟的数据地址,实际开发中需要替换为真实的接口地址。
4. 组件挂载时的操作
onMounted(() => {
getContentSize();
getListData(30);
});
在组件挂载后,首先调用 getContentSize 函数计算容器的最大容积,然后调用 getListData 函数获取 30 条初始数据。这样做是为了在组件首次渲染时,先确定容器能够容纳的数据项数量,再获取足够的数据进行初始渲染。
5. 计算容器最大容积
const getContentSize = () => {
if (scroll.value) {
contentSize.value =
Math.ceil(scroll.value.offsetHeight / oneHeight.value) + 1;
}
};
getContentSize 函数通过计算滚动容器的高度和单个数据项的高度,得出容器能够容纳的数据项数量。在计算之前,先检查 scroll.value 是否存在,避免在滚动容器还未挂载时出现错误。Math.ceil 函数用于向上取整,确保容器能够完整显示数据项,最后加 1 是为了在容器底部预留一个数据项的空间,避免出现滚动到底部时数据显示不全的问题。
6. 监听滚动事件
let isRequest = false;
let isScroll = true;
const handleScroll = async () => {
if (isScroll) {
isScroll = false;
const timer = setTimeout(() => {
isScroll = true;
clearTimeout(timer);
}, 30);
if (scroll.value) {
const currentIndex = ~~(scroll.value.scrollTop / oneHeight.value);
if (currentIndex === startIndex.value) return;
startIndex.value = currentIndex;
if (
startIndex.value + contentSize.value >= allList.value.length &&
!isRequest
) {
isRequest = true;
try {
await getListData();
} catch (error) {
console.error("加载更多数据失败:", error);
}
isRequest = false;
}
}
}
};
handleScroll 函数是实现无限滚动加载的核心。为了避免频繁触发滚动事件,使用了节流技术,通过 isScroll 变量控制。
在函数内部,先检查 scroll.value 是否存在,避免在滚动容器还未挂载时出现错误。通过 scroll.value.scrollTop 获取当前滚动的距离,除以单个数据项的高度 oneHeight.value,并使用 ~~ 进行取整操作,得到当前显示数据的起始索引 currentIndex。若 currentIndex 与 startIndex.value 相等,说明滚动位置没有发生变化,直接返回。
当滚动到接近列表底部时,即 startIndex.value + contentSize.value >= allList.value.length 且 !isRequest(表示当前没有正在进行的请求),会调用 getListData 函数加载更多数据。为避免重复请求,在请求开始前将 isRequest 设为 true,请求完成后再将其恢复为 false,同时添加 try...catch 语句捕获请求过程中可能出现的错误。
7. 计算显示数据的索引和样式
// 容器数据最后一条数据索引
const endIndex = computed(() => {
let endIndex = startIndex.value + contentSize.value * 2;
if (!allList.value[endIndex]) {
endIndex = allList.value.length - 1;
}
return endIndex;
});
// 容器展示数据
const showData = computed(() => {
let _startIndex = 0;
if (startIndex.value <= contentSize.value) {
_startIndex = 0;
} else {
_startIndex = startIndex.value - contentSize.value;
}
return allList.value.slice(_startIndex, endIndex.value);
});
// 上下空白区填充
const blankFillStyle = computed(() => {
let _startIndex = 0;
if (startIndex.value <= contentSize.value) {
_startIndex = 0;
} else {
_startIndex = startIndex.value - contentSize.value;
}
return {
paddingTop: _startIndex * oneHeight.value + "px",
paddingBottom:
(allList.value.length - endIndex.value) * oneHeight.value + "px",
};
});
为什么结束索引需要 startIndex + contentSize * 2
在虚拟列表的实现中,为保证滚动的流畅性,避免滚动过程中出现数据闪烁或者空白的情况,通常会多渲染一些数据,在当前可见区域的上下两侧形成缓冲区。
contentSize的含义:它代表滚动容器能够容纳的数据项数量,通过Math.ceil(scroll.value.offsetHeight / oneHeight.value) + 1计算得出。startIndex的含义:表示当前显示数据在allList中的起始索引,会随用户滚动列表动态更新。startIndex + contentSize * 2的作用:计算结束索引时使用该公式,是为了在当前可见区域的上下两侧各预留contentSize数量的数据项作为缓冲区。这样,当用户滚动列表时,即使滚动速度较快,也能保证有足够的数据显示,避免出现数据闪烁或空白。
例如,假设 contentSize 为 10,startIndex 为 20,那么 endIndex 就会计算为 20 + 10 * 2 = 40。此时,除了当前可见区域的 10 个数据项,还会额外渲染上下两侧各 10 个数据项,总共渲染 30 个数据项。
为什么 showData 要动态调整起始索引
if (startIndex.value <= contentSize.value) {
_startIndex = 0;
} else {
_startIndex = startIndex.value - contentSize.value;
}
这是为了解决滚动边界问题以及优化渲染范围。当滚动到顶部(startIndex 接近 0)时,直接从 0 开始渲染,避免出现负数索引或无效计算;当用户向下滚动时,起始索引逐步增加,通过减去 contentSize 可以确保渲染的数据包含当前可见区域以及上缓冲区的数据。
8. 模板部分
<template>
<div class="wrapper">
<div class="scroll-container" ref="scroll" @scroll.passive="handleScroll">
<div :style="blankFillStyle">
<div class="list-item" v-for="(item, index) in showData" :key="index">
<p class="title">{{ item.title }}</p>
<p class="content">{{ item.content }}</p>
<p class="reads">{{ item.reads }}</p>
</div>
</div>
</div>
</div>
</template>
模板部分使用了 v-for 指令遍历 showData,将每个数据项渲染成一个列表项。同时绑定了 scroll 事件,当滚动时调用 handleScroll 函数。@scroll.passive 表示使用被动事件监听器,可提高滚动性能。
9. 样式部分
<style lang="less" scoped>
.wrapper {
width: 100%;
height: 100%;
text-align: center;
.scroll-container {
width: 100%;
height: 100%;
overflow-y: auto;
.list-item {
margin: 4px 10px;
background-color: #eee;
height: 100px;
overflow: hidden;
.title {
font-size: 16px;
color: #333;
}
.content {
font-size: 14px;
color: #666;
}
.reads {
font-size: 12px;
color: #999;
}
}
}
}
</style>
样式部分使用了 Less 预处理器,为列表容器和列表项添加了一些基本的样式,让页面看起来更加美观。wrapper 类设置了宽度和高度为 100%,并将文本居中显示。scroll-container 类设置了宽度和高度为 100%,并添加了垂直滚动条。
总结
通过对这段代码的分析,我们了解了如何`实现无限滚动加载功能。关键在于监听滚动事件,计算当前显示数据的索引,以及在滚动到底部时加载更多数据。同时,利用缓冲区策略和动态调整起始索引的方式,保证了滚动的流畅性。还通过节流技术避免频繁触发滚动事件,提高性能。希望这篇博客能帮助你更好地理解前端开发中的无限滚动加载技术,让你在开发中更加得心应手!