设计思路
-
虚拟滚动原理:只渲染可见区域的数据行,通过计算滚动位置动态更新显示内容
-
性能优化:使用固定高度的行和高效的DOM操作,避免大量DOM节点造成的性能问题
-
用户体验:保持平滑滚动和准确滚动条位置
UI页面预览
完整的实现代码: (代码供参考,源码可直接运行)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>虚拟滚动表格 - 大数据量优化</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
padding: 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #2c3e50;
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 1px 1px 3px rgba(0,0,0,0.1);
}
.header p {
color: #7f8c8d;
font-size: 1.1rem;
max-width: 800px;
margin: 0 auto;
line-height: 1.6;
}
.controls {
background-color: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 25px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: center;
}
.control-group {
display: flex;
align-items: center;
gap: 10px;
}
label {
font-weight: 600;
color: #2c3e50;
}
input, select, button {
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1rem;
}
button {
background-color: #3498db;
color: white;
border: none;
cursor: pointer;
transition: background-color 0.3s;
font-weight: 600;
}
button:hover {
background-color: #2980b9;
}
.stats {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.stat-item {
background-color: white;
padding: 15px;
border-radius: 8px;
min-width: 180px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
}
.stat-value {
font-size: 1.8rem;
font-weight: 700;
color: #3498db;
}
.stat-label {
font-size: 0.9rem;
color: #7f8c8d;
margin-top: 5px;
}
.virtual-table-container {
background-color: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
margin-top: 30px;
}
.table-header {
display: flex;
background-color: #2c3e50;
color: white;
font-weight: 600;
position: sticky;
top: 0;
z-index: 10;
}
.table-header-cell {
padding: 16px 12px;
text-align: left;
flex: 1;
min-width: 120px;
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
.table-header-cell:last-child {
border-right: none;
}
.table-body {
height: 500px;
overflow-y: auto;
position: relative;
}
.table-row {
display: flex;
border-bottom: 1px solid #eee;
position: absolute;
left: 0;
right: 0;
transition: background-color 0.2s;
}
.table-row:hover {
background-color: #f8f9fa;
}
.table-cell {
padding: 14px 12px;
flex: 1;
min-width: 120px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.odd-row {
background-color: #f9f9f9;
}
.status-active {
color: #27ae60;
font-weight: 600;
}
.status-inactive {
color: #e74c3c;
font-weight: 600;
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #7f8c8d;
font-size: 1.2rem;
}
.scroll-info {
padding: 15px;
background-color: #f8f9fa;
border-top: 1px solid #eee;
font-size: 0.9rem;
color: #7f8c8d;
text-align: center;
}
.highlight {
background-color: #fffacd;
}
.performance-note {
margin-top: 30px;
padding: 20px;
background-color: white;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.performance-note h3 {
color: #2c3e50;
margin-bottom: 10px;
}
.performance-note ul {
padding-left: 20px;
line-height: 1.8;
color: #555;
}
.performance-note li {
margin-bottom: 8px;
}
@media (max-width: 768px) {
.controls {
flex-direction: column;
align-items: flex-start;
}
.table-header-cell, .table-cell {
min-width: 90px;
padding: 12px 8px;
}
.stat-item {
min-width: 140px;
}
}
</style>
</head>
<body>
<div id="app" class="container">
<div class="header">
<h1>虚拟滚动表格演示</h1>
<p>处理大规模数据时,通过虚拟滚动技术仅渲染可视区域的内容,避免因DOM节点过多导致的页面卡顿问题。</p>
</div>
<div class="controls">
<div class="control-group">
<label for="totalRows">数据总量:</label>
<input type="number" id="totalRows" v-model.number="totalRows" min="100" max="1000000" step="100">
</div>
<div class="control-group">
<label for="rowHeight">行高度(px):</label>
<input type="number" id="rowHeight" v-model.number="rowHeight" min="30" max="100" step="5">
</div>
<div class="control-group">
<label for="bufferRows">缓冲行数:</label>
<input type="number" id="bufferRows" v-model.number="bufferRows" min="0" max="20" step="1">
</div>
<div class="control-group">
<label for="filterStatus">状态筛选:</label>
<select id="filterStatus" v-model="filterStatus">
<option value="all">全部</option>
<option value="active">活跃</option>
<option value="inactive">非活跃</option>
</select>
</div>
<button @click="generateData">生成新数据</button>
<button @click="scrollToMiddle">滚动到中间</button>
<button @click="scrollToTop">回到顶部</button>
</div>
<div class="stats">
<div class="stat-item">
<div class="stat-value">{{ visibleRowCount }}</div>
<div class="stat-label">当前渲染行数</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ totalRowsFormatted }}</div>
<div class="stat-label">数据总量</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ renderTime.toFixed(2) }}ms</div>
<div class="stat-label">渲染时间</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ memoryUsage.toFixed(1) }}MB</div>
<div class="stat-label">内存占用</div>
</div>
</div>
<div class="virtual-table-container">
<div class="table-header">
<div class="table-header-cell">ID</div>
<div class="table-header-cell">姓名</div>
<div class="table-header-cell">邮箱</div>
<div class="table-header-cell">职位</div>
<div class="table-header-cell">部门</div>
<div class="table-header-cell">状态</div>
<div class="table-header-cell">入职日期</div>
</div>
<div class="table-body" ref="tableBody" @scroll="handleScroll">
<div :style="tableBodyStyle">
<div
v-for="row in visibleRows"
:key="row.id"
class="table-row"
:class="{ 'odd-row': row.index % 2 === 0, 'highlight': row.index === highlightIndex }"
:style="rowStyle(row.index)"
>
<div class="table-cell">{{ row.id }}</div>
<div class="table-cell">{{ row.name }}</div>
<div class="table-cell">{{ row.email }}</div>
<div class="table-cell">{{ row.position }}</div>
<div class="table-cell">{{ row.department }}</div>
<div class="table-cell" :class="row.status === 'active' ? 'status-active' : 'status-inactive'">
{{ row.status === 'active' ? '活跃' : '非活跃' }}
</div>
<div class="table-cell">{{ row.joinDate }}</div>
</div>
</div>
<div v-if="visibleRows.length === 0" class="empty-state">
暂无数据,请生成数据或调整筛选条件
</div>
</div>
<div class="scroll-info">
显示 {{ startIndex + 1 }} - {{ Math.min(endIndex, filteredData.length) }} 条,共 {{ filteredData.length }} 条数据
<span v-if="filteredData.length < totalRows"> (已筛选掉 {{ totalRows - filteredData.length }} 条)</span>
</div>
</div>
<div class="performance-note">
<h3>虚拟滚动技术优势:</h3>
<ul>
<li><strong>高性能渲染</strong>:仅渲染可视区域的数据行,无论总数据量多大,DOM节点数量保持恒定</li>
<li><strong>低内存占用</strong>:避免创建大量DOM节点,显著减少内存使用</li>
<li><strong>平滑滚动</strong>:滚动时仅更新已渲染行的数据,保持流畅的用户体验</li>
<li><strong>快速初始化</strong>:首次加载时不需要创建所有DOM节点,加快页面加载速度</li>
<li><strong>适用于移动端</strong>:在性能有限的移动设备上也能流畅运行</li>
</ul>
</div>
</div>
<script>
const { createApp, ref, computed, onMounted, watch } = Vue;
createApp({
setup() {
// 数据总量
const totalRows = ref(10000);
// 行高度
const rowHeight = ref(50);
// 缓冲行数(可视区域上下额外渲染的行数)
const bufferRows = ref(5);
// 表格容器引用
const tableBody = ref(null);
// 滚动位置
const scrollTop = ref(0);
// 容器高度
const containerHeight = ref(500);
// 渲染开始时间
const renderStartTime = ref(0);
// 渲染时间
const renderTime = ref(0);
// 高亮显示的行索引
const highlightIndex = ref(-1);
// 状态筛选
const filterStatus = ref('all');
// 生成的数据
const allData = ref([]);
// 生成模拟数据
const generateMockData = () => {
console.log(`生成 ${totalRows.value} 条模拟数据...`);
const startTime = performance.now();
const departments = ['技术部', '市场部', '人力资源部', '财务部', '产品部', '销售部', '运营部'];
const positions = ['工程师', '经理', '总监', '专员', '主管', '助理', '分析师'];
const firstNames = ['张', '王', '李', '赵', '刘', '陈', '杨', '黄', '周', '吴'];
const lastNames = ['伟', '芳', '娜', '秀英', '敏', '静', '丽', '强', '磊', '洋'];
const data = [];
for (let i = 1; i <= totalRows.value; i++) {
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)];
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)];
const name = firstName + lastName;
data.push({
id: i,
name: name,
email: `${name.toLowerCase()}@company.com`,
position: positions[Math.floor(Math.random() * positions.length)],
department: departments[Math.floor(Math.random() * departments.length)],
status: Math.random() > 0.3 ? 'active' : 'inactive',
joinDate: `${2020 + Math.floor(Math.random() * 4)}-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
index: i - 1 // 添加索引用于定位
});
}
const endTime = performance.now();
console.log(`数据生成完成,耗时 ${(endTime - startTime).toFixed(2)}ms`);
return data;
};
// 生成数据
const generateData = () => {
renderStartTime.value = performance.now();
allData.value = generateMockData();
highlightIndex.value = -1;
};
// 根据筛选条件过滤数据
const filteredData = computed(() => {
if (filterStatus.value === 'all') {
return allData.value;
} else {
return allData.value.filter(item => item.status === filterStatus.value);
}
});
// 格式化显示数据总量
const totalRowsFormatted = computed(() => {
if (totalRows.value >= 1000000) {
return (totalRows.value / 1000000).toFixed(1) + 'M';
} else if (totalRows.value >= 1000) {
return (totalRows.value / 1000).toFixed(1) + 'K';
}
return totalRows.value.toString();
});
// 计算可见行数
const visibleRowCount = computed(() => {
return Math.min(containerHeight.value / rowHeight.value + 2 * bufferRows.value, filteredData.value.length);
});
// 计算开始索引
const startIndex = computed(() => {
return Math.max(0, Math.floor(scrollTop.value / rowHeight.value) - bufferRows.value);
});
// 计算结束索引
const endIndex = computed(() => {
return Math.min(
filteredData.value.length,
startIndex.value + Math.ceil(containerHeight.value / rowHeight.value) + 2 * bufferRows.value
);
});
// 获取可见行数据
const visibleRows = computed(() => {
return filteredData.value.slice(startIndex.value, endIndex.value);
});
// 表格内容区域样式
const tableBodyStyle = computed(() => {
return {
height: `${filteredData.value.length * rowHeight.value}px`,
position: 'relative'
};
});
// 行样式
const rowStyle = (index) => {
return {
height: `${rowHeight.value}px`,
top: `${index * rowHeight.value}px`
};
};
// 处理滚动事件
const handleScroll = () => {
if (!tableBody.value) return;
scrollTop.value = tableBody.value.scrollTop;
// 计算当前可视区域中间的行索引
const middleIndex = Math.floor((scrollTop.value + containerHeight.value / 2) / rowHeight.value);
// 高亮显示中间行
if (middleIndex >= 0 && middleIndex < filteredData.value.length) {
highlightIndex.value = middleIndex;
}
};
// 滚动到中间位置
const scrollToMiddle = () => {
if (!tableBody.value) return;
const middlePosition = (filteredData.value.length * rowHeight.value - containerHeight.value) / 2;
tableBody.value.scrollTo({
top: middlePosition,
behavior: 'smooth'
});
};
// 滚动到顶部
const scrollToTop = () => {
if (!tableBody.value) return;
tableBody.value.scrollTo({
top: 0,
behavior: 'smooth'
});
};
// 模拟内存占用(粗略估算)
const memoryUsage = computed(() => {
// 每条数据大约占用200字节,转换为MB
return (allData.value.length * 200) / (1024 * 1024);
});
// 监听数据变化,更新渲染时间
watch([allData, filteredData], () => {
if (renderStartTime.value > 0) {
renderTime.value = performance.now() - renderStartTime.value;
}
}, { flush: 'post' });
// 监听筛选条件变化,重置滚动位置
watch(filterStatus, () => {
if (tableBody.value) {
tableBody.value.scrollTop = 0;
scrollTop.value = 0;
}
});
// 组件挂载时生成初始数据
onMounted(() => {
generateData();
// 监听窗口大小变化,更新容器高度
const updateContainerHeight = () => {
if (tableBody.value) {
containerHeight.value = tableBody.value.clientHeight;
}
};
updateContainerHeight();
window.addEventListener('resize', updateContainerHeight);
});
return {
totalRows,
rowHeight,
bufferRows,
tableBody,
scrollTop,
containerHeight,
renderTime,
highlightIndex,
filterStatus,
allData,
filteredData,
generateData,
totalRowsFormatted,
visibleRowCount,
startIndex,
endIndex,
visibleRows,
tableBodyStyle,
rowStyle,
handleScroll,
scrollToMiddle,
scrollToTop,
memoryUsage
};
}
}).mount('#app');
</script>
</body>
</html>
功能说明
1. 虚拟滚动实现:
只渲染可视区域及缓冲区的数据行
通过绝对定位和transform技术实现行位置定位
动态计算滚动位置,更新显示内容
2. 性能指标监控:
显示当前渲染的行数(通常只有几十行)
显示总数据量(可高达百万条)
显示渲染时间和内存占用估算
3. 交互功能:
可调整数据总量、行高、缓冲行数
可按状态筛选数据
支持滚动到中间位置或回到顶部
高亮显示当前可视区域中间的行
4. 性能优化说明:
虚拟滚动技术避免创建大量DOM节点
使用缓冲行减少滚动时的空白闪烁
高效的滚动事件处理
使用方法
-
将代码保存为HTML文件
-
直接在浏览器中打开文件
-
调整参数观察虚拟滚动的效果
这个实现可以处理高达百万级别的数据量,而不会造成页面卡顿,因为无论总数据量多少,实际渲染的DOM节点数量始终保持恒定,本示例是基于VUE3实现,其他框架原理相同。