在实际的开发中,可能因为用户需求或者数据导出,需要后端一次性返回大量数据,如:10w条,这个时候需要前端做处理,否则会造成页面卡顿、白屏,甚至崩溃。这是项目中可能遇到的问题,更是一道经典的面试题。下面我将介绍几种解决办法。
1.前端分页加载
- 数据分页,一次只加载一部分(如100条、200条、300条 ···);
- 可以结合element ui的分页器el-pagination一起使用,体验感更好;
- 本质上是前端模拟后端分页。
面试的时候都喜欢说,但博主感觉一般用不到,毕竟是特殊场景,分页能解决的可以直接让后端分页,简单、清晰、明了。
你说啥,后端不愿意处理,惯的了,拖出去da。。。
算了,最卑微的前端,还是说下怎么做吧,上代码。(为了照顾所有同学,博主直接写了一个完成的案例)
示例:
<template>
<div>
<el-table
:data="pageData"
stripe
border
style="width: 100%"
height="500"
>
<el-table-column prop="id" align="center" label="id"></el-table-column>
<el-table-column prop="name" align="center" label="姓名"></el-table-column>
<el-table-column prop="description" align="center" label="描述"></el-table-column>
<el-table-column prop="price" align="center" label="价格"></el-table-column>
<el-table-column prop="quantity" align="center" label="数量"></el-table-column>
</el-table>
<el-pagination
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
:current-page="currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</template>
<script>
export default {
name: "ceshis",
data() {
return {
allData: [], // 所有数据
pageData: [], // 当前表格页显示数据
total: 0, // 总数据量
currentPage: 1, // 当前页码
pageSize: 10 // 每页显示数量
};
},
methods: {
// 页面渲染
renderPage(pageVal) {
this.currentPage = pageVal; // 当前页码
const startIndex = (pageVal - 1) * this.pageSize; // 当前页开始索引
const endIndex = Math.min(startIndex + this.pageSize, this.total); // 当前页结束索引
this.pageData = this.allData.slice(startIndex, endIndex); // 截取当前页数据
},
// 生成模拟数据的函数(性能优化版本)
generateFakeData() {
let createdData = [];
for (let i = 0; i < 10000; i++) {
createdData.push({
id: i,
name: `Item ${i}`,
description: `Description for Item ${i}`,
price: (Math.random() * 100).toFixed(2),
quantity: Math.floor(Math.random() * 10),
});
}
console.log(createdData);
this.allData = createdData; // createdData后端接口返回的数据
this.total = createdData.length; // 后端返回总数据量
},
// 变化当前页
handleCurrentChange(pageVal) {
console.log('当前页:', pageVal)
this.renderPage(pageVal);
},
// 改变每页显示数量
handleSizeChange(sizeVal) {
console.log('每页显示数量:', sizeVal)
this.pageSize = sizeVal;
this.renderPage(1);
}
},
created() {
this.generateFakeData(); // 生成模拟数据
this.renderPage(1); // 渲染页面
},
};
</script>
核心代码(截取当前展示页数据):
const startIndex = (pageVal - 1) * this.pageSize; // 当前页开始索引
const endIndex = Math.min(startIndex + this.pageSize, this.total); // 当前页结束索引
this.pageData = this.allData.slice(startIndex, endIndex); // 截取当前页数据
2. setTimeout定时器加载
- 初始化加载一部分数据
- 每隔几百毫秒再加载一部分数据
- 减少一次渲染全部数据的压力
// 加载初始数据
loadInitialData() {
// this.loadBatchSize = 1000;
this.allData = this.totalData.slice(0, this.loadBatchSize); // 加载前 1000 条数据
this.loadedCount = this.loadBatchSize; // 更新已加载条数
this.loadMoreData(); // 开始加载更多数据
},
// 加载更多数据
loadMoreData() {
const loadNextBatch = () => {
if (this.loadedCount < this.totalData.length) {
const nextBatch = this.totalData.slice(this.loadedCount, this.loadedCount + this.loadBatchSize);
this.allData = this.allData.concat(nextBatch); // 合并新加载的数据
this.loadedCount += nextBatch.length; // 更新已加载条数
// 继续加载下一个批次
setTimeout(loadNextBatch, 300); // 每隔 300ms 加载下一批数据,可以根据需要调整时间间隔
}
};
loadNextBatch(); // 开始加载
},
缺点:全部数据加载完之前,浏览数据不丝滑,有卡顿
3. 浏览器自带的定时器requestAnimationFrame加载
- 用法和定时器类似
- 区别:不用传时间间隔
// 使用 requestAnimationFrame 进行下一次加载
requestAnimationFrame(loadNextBatch);
4. IntersectionObserver懒加载(非常推荐)
强烈推荐各位小伙伴学习使用,推荐大佬文章:掌握Intersection Observer API,轻松实现实现图片懒加载、元素滚动动画、无限滚动加载等功能-CSDN博客
- 浏览器自带的强大接口;
- 通过监听数据进入/离开视口加载数据;
- 代码简洁,性能好。
这里简单写了一个案例,但是为了页面美化,用cursor加了样式,代码比较多,可以直接复制运行,核心代码很少,并且都有注释。
<template>
<div class="order-info-container" :data-total="totalData.length">
<!-- 页面标题 -->
<h1 class="page-title">订单信息列表</h1>
<!-- 页面头部统计信息 -->
<div class="page-header">
<div class="stats-card">
<div class="stat-item">
<span class="stat-number">{{ totalData.length }}</span>
<span class="stat-label">总记录数</span>
</div>
<div class="stat-item">
<span class="stat-number">{{ allData.length }}</span>
<span class="stat-label">已加载</span>
</div>
<div class="stat-item">
<span class="stat-number">{{ Math.round((allData.length / totalData.length) * 100) }}%</span>
<span class="stat-label">加载进度</span>
</div>
</div>
</div>
<!-- 数据列表 -->
<div class="data-list">
<div v-for="(item, index) in allData" :key="index" class="list-item" :class="{ 'fade-in': true }">
<div class="item-id">
<span class="id-label">ID</span>
<span class="id-value">{{ item.id }}</span>
</div>
<div class="item-name">
<span class="name-label">名称</span>
<span class="name-value">{{ item.name }}</span>
</div>
<div class="item-description">
<span class="desc-label">描述</span>
<span class="desc-value">{{ item.description }}</span>
</div>
<div class="item-price">
<span class="price-label">价格</span>
<span class="price-value">¥{{ item.price }}</span>
</div>
<div class="item-quantity">
<span class="qty-label">数量</span>
<span class="qty-value">{{ item.quantity }}</span>
</div>
</div>
</div>
<!-- 加载指示器 -->
<div ref="loading" class="loading-indicator" v-show="hasMoreData || loading">
<div v-if="loading" class="loading-content">
<el-icon class="is-loading loading-icon"><Loading /></el-icon>
<span class="loading-text">正在加载更多数据...</span>
<div class="loading-progress">
<div class="progress-bar" :style="{ width: Math.round((allData.length / totalData.length) * 100) + '%' }"></div>
</div>
</div>
<div v-else-if="hasMoreData" class="scroll-hint">
<el-icon class="scroll-icon"><ArrowDown /></el-icon>
<span class="hint-text">向下滚动加载更多</span>
</div>
</div>
<!-- 无更多数据提示 -->
<div v-if="!hasMoreData && allData.length > 0" class="no-more-data">
<el-icon class="check-icon"><Check /></el-icon>
<span>已加载全部数据</span>
</div>
</div>
</template>
<script>
export default {
// 组件名称
name: "ceshis",
// 组件数据
data() {
return {
// 存储当前已加载并显示的数据
allData: [],
// 存储全部数据
totalData: [],
// 已加载的数据数量
loadedCount: 0,
// 每次加载的数据批次大小
loadBatchSize: 50,
// 是否正在加载数据的标志
loading: false,
// IntersectionObserver 实例,用于监听元素是否进入视口
observer: null,
};
},
// 计算属性
computed: {
// 判断是否还有更多数据未加载
hasMoreData() {
return this.loadedCount < this.totalData.length;
}
},
// 组件方法
methods: {
// 生成假数据用于测试
generateFakeData() {
// 生成10000条假数据
for (let i = 0; i < 10000; i++) {
this.totalData.push({
id: i+1,
name: `Item ${i}`,
description: `Description for Item ${i}`,
price: (Math.random() * 100).toFixed(2),
quantity: Math.floor(Math.random() * 10),
});
}
// 加载初始数据
this.loadInitialData();
},
// 加载初始数据
loadInitialData() {
// 从全部数据中截取第一批数据
const initialData = this.totalData.slice(0, this.loadBatchSize);
// 设置显示的数据为初始数据
this.allData = initialData;
// 更新已加载数据计数
this.loadedCount = initialData.length;
// 输出日志
console.log("已加载数据:", initialData);
},
// 加载更多数据
loadMoreData() {
console.log("开始加载更多数据...")
// 如果没有更多数据或正在加载中,则返回
if (!this.hasMoreData || this.loading) {
console.log("没有更多数据或正在加载中");
return;
}
// 设置加载状态为true
this.loading = true;
console.log(`当前已加载: ${this.loadedCount}, 总数: ${this.totalData.length}`);
// 模拟异步加载数据
setTimeout(() => {
// 截取下一批数据
const nextBatch = this.totalData.slice(this.loadedCount, this.loadedCount + this.loadBatchSize);
// 将新数据追加到已显示的数据中
this.allData = [...this.allData, ...nextBatch];
// 更新已加载数据计数
this.loadedCount += nextBatch.length;
// 设置加载状态为false
this.loading = false;
console.log(`加载完成,当前已加载: ${this.loadedCount}`);
}, 300);
},
// 设置IntersectionObserver监听器
setupObserver() {
console.log('设置 IntersectionObserver');
// 如果已有observer实例,则先断开连接
if (this.observer) {
this.observer.disconnect();
}
// 创建新的IntersectionObserver实例
this.observer = new IntersectionObserver((entries) => {
console.log("IntersectionObserver 触发:", entries);
// 遍历观察的元素
entries.forEach(entry => {
// 如果元素进入视口且还有更多数据且未在加载中
if (entry.isIntersecting && this.hasMoreData && !this.loading) {
console.log("触发懒加载");
// 加载更多数据
this.loadMoreData();
}
});
}, {
// 根元素为视口
root: null,
// 根边距为100px,提前100px触发加载
rootMargin: '100px',
// 交叉比例达到0.1时触发
threshold: 0.1,
});
// 在下次DOM更新后执行
this.$nextTick(() => {
// 如果loading元素存在
if (this.$refs.loading) {
console.log("开始观察 loading 元素:", this.$refs.loading);
// 开始观察loading元素
this.observer.observe(this.$refs.loading);
} else {
console.error("loading 元素不存在");
}
});
},
},
// 组件挂载完成后执行
mounted() {
// 生成假数据
this.generateFakeData();
// 设置观察器
this.setupObserver();
},
// 组件销毁前执行
beforeDestroy() {
// 如果observer存在,则断开连接
if (this.observer) {
this.observer.disconnect();
}
},
};
</script>
<style scoped>
.order-info-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* 页面头部样式 */
.page-header {
margin-bottom: 30px;
}
.stats-card {
display: flex;
justify-content: space-around;
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.stat-number {
font-size: 32px;
font-weight: 700;
color: #667eea;
margin-bottom: 8px;
}
.stat-label {
font-size: 14px;
color: #666;
font-weight: 500;
}
/* 数据列表样式 */
.data-list {
margin-bottom: 20px;
}
.list-item {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
display: grid;
grid-template-columns: 100px 1fr 2fr 120px 100px;
gap: 20px;
align-items: center;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
animation: fadeInUp 0.6s ease-out;
}
.list-item:hover {
transform: translateY(-3px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
background: rgba(255, 255, 255, 1);
}
.list-item > div {
display: flex;
flex-direction: column;
gap: 4px;
}
.list-item .id-label,
.list-item .name-label,
.list-item .desc-label,
.list-item .price-label,
.list-item .qty-label {
font-size: 12px;
color: #999;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.list-item .id-value {
font-size: 18px;
font-weight: 700;
color: #667eea;
}
.list-item .name-value {
font-size: 16px;
font-weight: 600;
color: #2c3e50;
}
.list-item .desc-value {
font-size: 14px;
color: #7f8c8d;
font-style: italic;
line-height: 1.4;
}
.list-item .price-value {
font-size: 16px;
font-weight: 700;
color: #e74c3c;
text-align: right;
}
.list-item .qty-value {
font-size: 16px;
font-weight: 700;
color: #27ae60;
text-align: center;
background: rgba(39, 174, 96, 0.1);
padding: 6px 12px;
border-radius: 8px;
border: 1px solid rgba(39, 174, 96, 0.2);
}
.loading-indicator {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
margin-top: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.loading-icon {
font-size: 32px;
color: #667eea;
animation: spin 1s linear infinite;
}
.loading-text {
color: #666;
font-size: 16px;
font-weight: 500;
text-align: center;
}
.loading-progress {
width: 200px;
height: 6px;
background: rgba(102, 126, 234, 0.2);
border-radius: 3px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 3px;
transition: width 0.3s ease;
}
.scroll-hint {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.scroll-icon {
font-size: 24px;
color: #667eea;
animation: bounce 2s infinite;
}
.hint-text {
color: #666;
font-size: 14px;
font-weight: 500;
text-align: center;
}
.no-more-data {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 30px 20px;
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
margin-top: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #27ae60;
font-size: 16px;
font-weight: 500;
}
.check-icon {
font-size: 20px;
color: #27ae60;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-10px);
}
60% {
transform: translateY(-5px);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式设计 */
@media (max-width: 1024px) {
.stats-card {
padding: 20px;
}
.stat-number {
font-size: 28px;
}
.list-item {
grid-template-columns: 80px 1fr 1.5fr 100px 80px;
gap: 16px;
padding: 16px;
}
}
@media (max-width: 768px) {
.order-info-container {
padding: 15px;
}
.stats-card {
flex-direction: column;
gap: 20px;
padding: 20px;
}
.stat-item {
flex-direction: row;
justify-content: space-between;
width: 100%;
}
.stat-number {
font-size: 24px;
margin-bottom: 0;
}
.list-item {
grid-template-columns: 1fr;
gap: 12px;
padding: 16px;
}
.list-item > div {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.list-item .id-label,
.list-item .name-label,
.list-item .desc-label,
.list-item .price-label,
.list-item .qty-label {
font-size: 11px;
}
.list-item .id-value,
.list-item .name-value,
.list-item .desc-value,
.list-item .price-value,
.list-item .qty-value {
font-size: 14px;
}
}
@media (max-width: 480px) {
.order-info-container {
padding: 10px;
}
.stats-card {
padding: 16px;
}
.stat-number {
font-size: 20px;
}
.stat-label {
font-size: 12px;
}
.list-item {
padding: 12px;
margin-bottom: 12px;
}
.list-item > div {
gap: 8px;
}
.loading-indicator {
padding: 24px 16px;
}
.loading-progress {
width: 150px;
}
}
/* 页面标题样式 */
.page-title {
text-align: center;
font-size: 32px;
font-weight: 700;
color: white;
margin-bottom: 30px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
letter-spacing: 1px;
}
/* 添加滚动条样式 */
.order-info-container::-webkit-scrollbar {
width: 8px;
}
.order-info-container::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.order-info-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
.order-info-container::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
</style>
注:此方法不太适用于表格加载,有很多的小问题。
总结
方法 | 优点 | 缺点 |
分页 | 简单清晰明了 | 一般生产环境用不到 |
setTimeout定时器 | 初始加载一部分数据用于“糊弄”用户,几秒之后可以加载全部数据,可用于数据导出 | 数据加载完之前页面卡断,滑动数据不流畅 |
requestAnimationFrame | 浏览器自带,使用方便 | IE10以下不支持 |
InterSectionObserver | 性能高,适用范围广,一劳永逸 | 新API,旧版本不支持 Chrome:57+、Firefox:55+、Safari:12.1+、Edge:15+、Opera:44+,IE:不支持 |
前端处理大量数据的方法有很多,这里我推荐了我知道的几种,博主第一篇博客,既是记录,也是分享,其他方法欢迎补充,一起学习。