大白话在vue2和vue3中展示实现无限滚动加载数据列表的代码示例,分析数据分页和加载状态处理逻辑
前端小伙伴们,有没有被“长列表加载”搞到头疼过?用户刷手机时,列表滚到底部半天没反应;或者手一抖滚太快,触发了N次重复请求;更尴尬的是——加载中的“转圈”图标突然消失,页面卡成PPT……今天咱们就用Vue2和Vue3,手把手教你实现丝滑的无限滚动加载,彻底解决这些糟心事!
一、长列表的"加载之痛"
先说说我做电商项目时踩的坑:商品列表用v-for
直接渲染1000条数据,页面卡到用户投诉“划不动”;后来改成翻页按钮,用户又吐槽“每次都要手动点,麻烦”;最离谱的是——滚动到底部触发加载,结果网络慢的时候,用户重复滚动触发了3次请求,数据乱成一团。
这些痛点总结起来就3条:
- 性能卡顿:一次性渲染大量数据,页面响应慢;
- 重复请求:滚动过快导致多次触发加载;
- 状态混乱:加载中的提示、加载完成的文案没管好,用户体验差。
二、无限滚动的3个核心逻辑
无限滚动的本质是“滚动触发加载”,核心要解决3个问题:何时加载、如何加载、如何控制状态。
1. 何时加载:滚动位置的判断
判断是否需要加载新数据,关键是计算“滚动条是否接近底部”。公式如下:
// 触发加载的条件:滚动高度 + 视口高度 ≥ 内容总高度 - 阈值(比如50px)
const shouldLoad = scrollTop + clientHeight >= scrollHeight - threshold;
scrollTop
:滚动条距离顶部的距离;clientHeight
:视口高度(可见区域的高度);scrollHeight
:内容总高度(包括不可见部分);threshold
:阈值(提前50px触发加载,避免用户滚到底才开始加载)。
2. 如何加载:数据分页与防抖
为了避免重复请求,需要:
- 分页控制:用
page
变量记录当前页码,每次加载后page++
; - 防抖处理:用
lodash.debounce
或自定义函数,确保滚动事件在短时间内只触发一次; - 请求锁:用
isLoading
状态标记“正在加载”,防止重复请求。
3. 如何控制状态:加载提示与边界处理
用户需要明确的反馈:
- 加载中:显示“正在加载…”的提示;
- 加载完成:显示“没有更多数据了”;
- 加载失败:显示“加载失败,点击重试”。
三、代码示例:Vue2和Vue3的实现对比
(一)Vue2实现:选项式API版本
Vue2使用mounted
和beforeDestroy
生命周期管理滚动事件,用data
存储状态,methods
处理逻辑。
<template>
<div class="infinite-list">
<!-- 数据列表 -->
<div class="list-item" v-for="item in list" :key="item.id">
{{ item.content }}
</div>
<!-- 加载状态提示 -->
<div class="load-status">
<!-- 加载中 -->
<div v-if="isLoading">⏳ 正在加载第{{ page }}页...</div>
<!-- 加载完成 -->
<div v-else-if="hasMore">👇 继续滚动加载更多</div>
<!-- 无更多数据 -->
<div v-else>✨ 已加载全部数据</div>
<!-- 加载失败 -->
<div v-if="loadError" class="error-tip" @click="reload">
⚠️ 加载失败,点击重试
</div>
</div>
</div>
</template>
<script>
import { debounce } from 'lodash'; // 引入防抖函数
export default {
data() {
return {
list: [], // 数据列表
page: 1, // 当前页码
isLoading: false, // 加载状态
hasMore: true, // 是否有更多数据
loadError: false, // 加载失败状态
threshold: 50, // 触发加载的阈值(px)
};
},
mounted() {
// 挂载时添加滚动监听(防抖处理,300ms内只触发一次)
this.scrollHandler = debounce(this.checkScroll, 300);
window.addEventListener('scroll', this.scrollHandler);
// 初始加载第一页
this.loadData();
},
beforeDestroy() {
// 销毁时移除滚动监听,避免内存泄漏
window.removeEventListener('scroll', this.scrollHandler);
},
methods: {
// 检查滚动位置
checkScroll() {
// 如果正在加载或无更多数据,直接返回
if (this.isLoading || !this.hasMore) return;
// 获取滚动相关数值
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const clientHeight = document.documentElement.clientHeight;
const scrollHeight = document.documentElement.scrollHeight;
// 判断是否触发加载
if (scrollTop + clientHeight >= scrollHeight - this.threshold) {
this.loadData();
}
},
// 加载数据
async loadData() {
this.isLoading = true; // 标记加载中
this.loadError = false; // 重置错误状态
try {
// 模拟API请求(实际替换为你的接口)
const res = await fetch(`https://api.example.com/data?page=${this.page}`);
const data = await res.json();
// 没有更多数据:如果返回数据为空,或接口返回hasMore为false
if (data.length === 0 || !data.hasMore) {
this.hasMore = false;
} else {
this.list = this.list.concat(data); // 合并新数据
this.page++; // 页码+1
}
} catch (error) {
this.loadError = true; // 标记加载失败
console.error('加载失败:', error);
} finally {
this.isLoading = false; // 无论成功失败,都要结束加载状态
}
},
// 重试加载(加载失败时调用)
reload() {
this.loadData();
},
},
};
</script>
<style scoped>
.infinite-list {
min-height: 100vh; /* 确保列表有足够高度触发滚动 */
}
.list-item {
padding: 16px;
border-bottom: 1px solid #e5e7eb;
}
.load-status {
padding: 16px;
text-align: center;
color: #6b7280;
}
.error-tip {
color: #ef4444;
cursor: pointer;
}
</style>
(二)Vue3实现:组合式API版本
Vue3使用setup
函数和ref/reactive
管理状态,用onMounted/onUnmounted
生命周期,代码更简洁、逻辑更集中。
<template>
<!-- 模板结构与Vue2完全一致 -->
<div class="infinite-list">
<div class="list-item" v-for="item in list" :key="item.id">
{{ item.content }}
</div>
<div class="load-status">
<div v-if="isLoading">⏳ 正在加载第{{ page }}页...</div>
<div v-else-if="hasMore">👇 继续滚动加载更多</div>
<div v-else>✨ 已加载全部数据</div>
<div v-if="loadError" class="error-tip" @click="reload">
⚠️ 加载失败,点击重试
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { debounce } from 'lodash';
// 响应式状态
const list = ref([]);
const page = ref(1);
const isLoading = ref(false);
const hasMore = ref(true);
const loadError = ref(false);
const threshold = ref(50);
// 滚动监听函数(防抖处理)
const checkScroll = debounce(() => {
if (isLoading.value || !hasMore.value) return;
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const clientHeight = document.documentElement.clientHeight;
const scrollHeight = document.documentElement.scrollHeight;
if (scrollTop + clientHeight >= scrollHeight - threshold.value) {
loadData();
}
}, 300);
// 加载数据函数
const loadData = async () => {
isLoading.value = true;
loadError.value = false;
try {
const res = await fetch(`https://api.example.com/data?page=${page.value}`);
const data = await res.json();
if (data.length === 0 || !data.hasMore) {
hasMore.value = false;
} else {
list.value = [...list.value, ...data]; // 合并新数据(Vue3推荐用展开语法)
page.value++;
}
} catch (error) {
loadError.value = true;
console.error('加载失败:', error);
} finally {
isLoading.value = false;
}
};
// 重试加载
const reload = () => {
loadData();
};
// 生命周期:挂载时初始化
onMounted(() => {
window.addEventListener('scroll', checkScroll);
loadData(); // 初始加载
});
// 生命周期:卸载时清理
onUnmounted(() => {
window.removeEventListener('scroll', checkScroll);
});
</script>
<style scoped>
/* 样式与Vue2一致,这里省略 */
</style>
四、Vue2 vs Vue3实现差异
对比项 | Vue2(选项式API) | Vue3(组合式API) |
---|---|---|
状态管理 | 分散在data 对象中 | 集中在ref/reactive 中,逻辑更紧凑 |
生命周期 | mounted /beforeDestroy | onMounted /onUnmounted |
代码结构 | 按类型(data/methods)组织 | 按功能(滚动逻辑/加载逻辑)组织 |
响应式更新 | 依赖Object.defineProperty | 基于Proxy ,支持更多数据类型 |
维护性 | 逻辑分散,长组件难以维护 | 逻辑模块化,易于拆分复用 |
性能 | 良好,但复杂场景可能有性能损耗 | 更高效的响应式系统,性能更优 |
五、面试题回答方法
正常回答(结构化):
“无限滚动加载的核心是监听滚动事件,判断是否到达加载位置,结合分页逻辑请求数据,并控制加载状态。具体步骤:
- 滚动监听:通过
addEventListener('scroll', handler)
监听滚动事件,用防抖(debounce
)优化性能;- 位置判断:计算
scrollTop + clientHeight ≥ scrollHeight - threshold
,触发加载;- 分页控制:用
page
变量记录当前页码,每次加载后递增;- 状态管理:用
isLoading
防止重复请求,hasMore
判断是否有更多数据,loadError
处理加载失败;
Vue2和Vue3的差异主要体现在API风格:Vue2用选项式API(data
/methods
),Vue3用组合式API(setup
/ref
),后者逻辑更集中,维护更方便。”
大白话回答(接地气):
“就像自动续杯的奶茶——用户滚到底部,系统自动‘续杯’下一页数据。关键是要判断‘杯子快空了’(滚动接近底部),然后‘续杯’(请求数据),同时告诉用户‘正在倒奶茶’(加载中)、‘奶茶倒完了’(无更多数据)。
Vue2和Vue3的区别就像用‘老式收音机’还是‘智能手机’:Vue2功能都有,但按钮分散;Vue3把功能集成到‘屏幕’上(组合式API),用起来更顺手。”
六、总结:3个核心步骤+2个避坑指南
3个核心步骤:
- 滚动监听:用防抖优化,避免频繁触发;
- 分页请求:控制
page
变量,合并新数据; - 状态反馈:明确加载中、加载完成、加载失败的提示。
2个避坑指南:
- 避免内存泄漏:一定要在组件卸载时移除滚动监听(
removeEventListener
); - 处理动态高度:如果列表项高度不固定,用
IntersectionObserver
替代滚动监听(下文扩展思考会讲)。
七、扩展思考:4个高频问题解答
问题1:列表项高度动态变化,滚动监听不准怎么办?
解答:用IntersectionObserver
(交叉观察者)监听“加载触发区”是否可见。在列表底部加一个div.load-trigger
,当它进入视口时触发加载:
<template>
<div class="infinite-list">
<!-- 列表项 -->
<div class="list-item" v-for="item in list" :key="item.id">...</div>
<!-- 触发加载的占位元素 -->
<div ref="trigger" class="load-trigger"></div>
<!-- 状态提示 -->
<div class="load-status">...</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const trigger = ref(null);
const observer = ref(null);
onMounted(() => {
// 创建交叉观察者,阈值设为0(触发区进入视口即触发)
observer.value = new IntersectionObserver((entries) => {
const entry = entries[0];
if (entry.isIntersecting && !isLoading.value && hasMore.value) {
loadData();
}
}, { threshold: 0 });
// 观察触发区
observer.value.observe(trigger.value);
});
onUnmounted(() => {
observer.value.disconnect(); // 卸载时断开观察者
});
</script>
问题2:如何优化滚动性能?
解答:
- 虚拟滚动:只渲染可见区域的列表项(如
vue-virtual-scroller
库),减少DOM节点数量; - 节流/防抖:滚动事件用
debounce
或throttle
限制触发频率; - 避免强制同步布局:不要在滚动事件中读取
scrollTop
等会触发重排的属性(可缓存上一次的值)。
问题3:如何实现反向滚动加载(向上滚动加载历史数据)?
解答:监听scrollTop === 0
(滚动到顶部),并判断是否需要加载上一页数据:
// 在checkScroll函数中添加
if (scrollTop === 0 && !isLoading.value && hasPrev) {
loadPrevData(); // 加载上一页
}
问题4:如何与UI库(如Element UI)结合使用?
解答:UI库的el-scrollbar
组件自带滚动事件,用@scroll
监听即可,无需操作原生window
对象:
<el-scrollbar @scroll="handleScroll">
<div class="infinite-list">...</div>
</el-scrollbar>
<script>
// Vue2示例
methods: {
handleScroll({ target }) {
// target是el-scrollbar的滚动容器
const scrollTop = target.scrollTop;
const clientHeight = target.clientHeight;
const scrollHeight = target.scrollHeight;
// 判断加载条件...
}
}
</script>
结尾:无限滚动的终极目标——“无感加载”
好的无限滚动应该让用户“察觉不到加载”:数据无缝衔接,提示清晰明确,滚动流畅不卡顿。通过Vue2和Vue3的实现对比,我们能看到Vue3在逻辑组织上的优势,但核心原理是相通的。下次再遇到长列表需求,你可以拍着胸脯说:“这题我会,5分钟搞定!”
如果这篇文章帮你理清了思路,别忘了点个赞~ 有任何问题,评论区见!咱们下期聊“如何用Vue3组合式API重构复杂表单”,不见不散!