A:动画及测试文件
This commit is contained in:
1 parent
f6d1409295
commit
7716850e16
17 files changed
+11653
No files matched your search
@@ -0,0 +1,726 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>全局虚拟滚动列表</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background-color: #f5f5f7;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background-color: #fff;
|
||||
min-height: 100vh;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.page-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
|
||||
animation: shimmer 3s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.list-section {
|
||||
position: relative;
|
||||
border-bottom: 1px solid #f1f3f5;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
background: #f8f9fa;
|
||||
padding: 15px 20px;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: #495057;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(248, 249, 250, 0.95);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.list-header:hover {
|
||||
background: rgba(248, 249, 250, 1);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.list-header span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list-header span::before {
|
||||
content: '📋';
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.list-header::after {
|
||||
content: '▼';
|
||||
font-size: 12px;
|
||||
transition: transform 0.3s ease;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.list-header.collapsed::after {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.virtual-list-container {
|
||||
position: relative;
|
||||
transition: max-height 0.3s ease-out;
|
||||
}
|
||||
|
||||
.virtual-list-container.collapsed {
|
||||
max-height: 0 !important;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease-in;
|
||||
}
|
||||
|
||||
.virtual-list-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.virtual-list-spacer {
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.virtual-list-viewport {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.virtual-list-item {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #f1f3f5;
|
||||
transition: background-color 0.2s ease, padding-left 0.2s ease;
|
||||
background: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.virtual-list-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 3px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.virtual-list-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.virtual-list-item:hover::before {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.item-number {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
text-align: center;
|
||||
line-height: 28px;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 不同分类的颜色主题 */
|
||||
.list-section:nth-child(2n) .list-header {
|
||||
background: rgba(255, 243, 224, 0.95);
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.list-section:nth-child(2n) .list-header span::before {
|
||||
content: '🍔';
|
||||
}
|
||||
|
||||
.list-section:nth-child(3n) .list-header {
|
||||
background: rgba(224, 255, 224, 0.95);
|
||||
color: #51cf66;
|
||||
}
|
||||
|
||||
.list-section:nth-child(3n) .list-header span::before {
|
||||
content: '🥗';
|
||||
}
|
||||
|
||||
.list-section:nth-child(4n) .list-header {
|
||||
background: rgba(224, 240, 255, 0.95);
|
||||
color: #339af0;
|
||||
}
|
||||
|
||||
.list-section:nth-child(4n) .list-header span::before {
|
||||
content: '🍰';
|
||||
}
|
||||
|
||||
.list-section:nth-child(5n) .list-header {
|
||||
background: rgba(255, 224, 255, 0.95);
|
||||
color: #ae3ec9;
|
||||
}
|
||||
|
||||
.list-section:nth-child(5n) .list-header span::before {
|
||||
content: '🥤';
|
||||
}
|
||||
|
||||
/* 滚动提示 */
|
||||
.scroll-indicator {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 10px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scroll-indicator.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.performance-info {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 15px;
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
z-index: 1000;
|
||||
max-width: 300px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
padding: 12px 15px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.virtual-list-item {
|
||||
padding: 12px 15px;
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
.performance-info {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="performance-info" id="performanceInfo">
|
||||
渲染: 0 / 0 项目
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="page-header">
|
||||
<h1>全局虚拟滚动列表演示</h1>
|
||||
<p style="font-size: 14px; margin-top: 10px; opacity: 0.9;">支持大量数据,只有一个全局滚动条</p>
|
||||
</div>
|
||||
|
||||
<div id="listContainer"></div>
|
||||
</div>
|
||||
|
||||
<div class="scroll-indicator" id="scrollIndicator">继续滚动查看更多</div>
|
||||
|
||||
<script>
|
||||
// 防抖函数
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// 优化的节流函数
|
||||
function throttle(func, delay) {
|
||||
let timeoutId;
|
||||
let lastExecTime = 0;
|
||||
|
||||
return function (...args) {
|
||||
const currentTime = Date.now();
|
||||
|
||||
if (currentTime - lastExecTime > delay) {
|
||||
func.apply(this, args);
|
||||
lastExecTime = currentTime;
|
||||
} else {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
func.apply(this, args);
|
||||
lastExecTime = Date.now();
|
||||
}, delay - (currentTime - lastExecTime));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class GlobalVirtualList {
|
||||
constructor(container, data, options = {}) {
|
||||
this.container = container;
|
||||
this.data = data;
|
||||
this.itemHeight = options.itemHeight || 60;
|
||||
this.isCollapsed = options.collapsed || false;
|
||||
this.startIndex = 0;
|
||||
this.endIndex = 0;
|
||||
this.bufferSize = 10;
|
||||
this.listId = Math.random().toString(36).substr(2, 9);
|
||||
|
||||
// 稳定性优化参数
|
||||
this.lastScrollTop = 0;
|
||||
this.scrollDirection = 'down';
|
||||
this.stableRangeCache = null;
|
||||
this.lastRenderTime = 0;
|
||||
this.minRenderInterval = 50; // 最小渲染间隔
|
||||
|
||||
this.init();
|
||||
this.render();
|
||||
}
|
||||
|
||||
init() {
|
||||
// 创建虚拟列表容器
|
||||
this.listContainer = document.createElement('div');
|
||||
this.listContainer.className = 'virtual-list-container';
|
||||
|
||||
if (this.isCollapsed) {
|
||||
this.listContainer.classList.add('collapsed');
|
||||
}
|
||||
|
||||
// 创建包装器
|
||||
this.wrapper = document.createElement('div');
|
||||
this.wrapper.className = 'virtual-list-wrapper';
|
||||
|
||||
// 创建占位空间元素(维持总高度)
|
||||
this.spacer = document.createElement('div');
|
||||
this.spacer.className = 'virtual-list-spacer';
|
||||
this.spacer.style.height = `${this.data.length * this.itemHeight}px`;
|
||||
|
||||
// 创建视口
|
||||
this.viewport = document.createElement('div');
|
||||
this.viewport.className = 'virtual-list-viewport';
|
||||
|
||||
this.wrapper.appendChild(this.spacer);
|
||||
this.wrapper.appendChild(this.viewport);
|
||||
this.listContainer.appendChild(this.wrapper);
|
||||
this.container.appendChild(this.listContainer);
|
||||
}
|
||||
|
||||
// 获取绝对位置
|
||||
getAbsoluteOffset() {
|
||||
const rect = this.wrapper.getBoundingClientRect();
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
return rect.top + scrollTop;
|
||||
}
|
||||
|
||||
getVisibleRange() {
|
||||
if (this.isCollapsed) {
|
||||
return { start: 0, end: 0 };
|
||||
}
|
||||
|
||||
// 获取当前滚动位置
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
// 检测滚动方向
|
||||
this.scrollDirection = scrollTop > this.lastScrollTop ? 'down' : 'up';
|
||||
this.lastScrollTop = scrollTop;
|
||||
|
||||
// 获取容器的绝对位置
|
||||
const containerTop = this.getAbsoluteOffset();
|
||||
const containerHeight = this.data.length * this.itemHeight;
|
||||
|
||||
// 计算可见区域
|
||||
const viewportTop = scrollTop;
|
||||
const viewportBottom = scrollTop + windowHeight;
|
||||
|
||||
// 计算列表区域
|
||||
const listTop = containerTop;
|
||||
const listBottom = containerTop + containerHeight;
|
||||
|
||||
// 检查是否有交集
|
||||
if (listBottom <= viewportTop || listTop >= viewportBottom) {
|
||||
return { start: 0, end: 0 };
|
||||
}
|
||||
|
||||
// 计算可见范围
|
||||
const visibleStartInList = Math.max(0, viewportTop - listTop);
|
||||
const visibleEndInList = Math.min(containerHeight, viewportBottom - listTop);
|
||||
|
||||
// 计算项目索引 - 使用稳定的整数运算
|
||||
let startIndex = Math.floor(visibleStartInList / this.itemHeight);
|
||||
let endIndex = Math.ceil(visibleEndInList / this.itemHeight);
|
||||
|
||||
// 确保索引在有效范围内
|
||||
startIndex = Math.max(0, Math.min(startIndex, this.data.length - 1));
|
||||
endIndex = Math.max(startIndex + 1, Math.min(endIndex, this.data.length));
|
||||
|
||||
// 动态调整缓冲区大小
|
||||
const dynamicBuffer = this.scrollDirection === 'down'
|
||||
? { top: Math.floor(this.bufferSize * 0.3), bottom: this.bufferSize }
|
||||
: { top: this.bufferSize, bottom: Math.floor(this.bufferSize * 0.3) };
|
||||
|
||||
// 添加缓冲区
|
||||
const bufferedStart = Math.max(0, startIndex - dynamicBuffer.top);
|
||||
const bufferedEnd = Math.min(this.data.length, endIndex + dynamicBuffer.bottom);
|
||||
|
||||
return {
|
||||
start: bufferedStart,
|
||||
end: bufferedEnd
|
||||
};
|
||||
}
|
||||
|
||||
shouldUpdate(newStart, newEnd) {
|
||||
// 时间限流
|
||||
const now = Date.now();
|
||||
if (now - this.lastRenderTime < this.minRenderInterval) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果范围完全相同,不更新
|
||||
if (newStart === this.startIndex && newEnd === this.endIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 计算重叠区域
|
||||
const overlapStart = Math.max(this.startIndex, newStart);
|
||||
const overlapEnd = Math.min(this.endIndex, newEnd);
|
||||
const overlapSize = Math.max(0, overlapEnd - overlapStart);
|
||||
const currentSize = this.endIndex - this.startIndex;
|
||||
const newSize = newEnd - newStart;
|
||||
|
||||
// 如果重叠区域占当前显示区域的大部分,且变化不大,跳过更新
|
||||
if (currentSize > 0 && overlapSize / currentSize > 0.7) {
|
||||
const startDiff = Math.abs(newStart - this.startIndex);
|
||||
const endDiff = Math.abs(newEnd - this.endIndex);
|
||||
|
||||
// 如果变化很小,跳过
|
||||
if (startDiff <= 2 && endDiff <= 2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { start, end } = this.getVisibleRange();
|
||||
|
||||
// 检查是否需要更新
|
||||
if (!this.shouldUpdate(start, end)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`List ${this.listId} 渲染范围: ${start} - ${end} (之前: ${this.startIndex} - ${this.endIndex})`);
|
||||
|
||||
this.startIndex = start;
|
||||
this.endIndex = end;
|
||||
this.lastRenderTime = Date.now();
|
||||
|
||||
// 清除现有项目
|
||||
this.viewport.innerHTML = '';
|
||||
|
||||
// 如果折叠状态或没有可见项目,直接返回
|
||||
if (this.isCollapsed || start >= end) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建文档片段提高性能
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
// 渲染可见项目
|
||||
for (let i = start; i < end && i < this.data.length; i++) {
|
||||
const item = this.createItem(this.data[i], i);
|
||||
item.style.transform = `translateY(${i * this.itemHeight}px)`;
|
||||
fragment.appendChild(item);
|
||||
}
|
||||
|
||||
this.viewport.appendChild(fragment);
|
||||
}
|
||||
|
||||
createItem(data, index) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'virtual-list-item';
|
||||
item.innerHTML = `
|
||||
<span class="item-number">${index + 1}</span>
|
||||
<span class="item-content">${data}</span>
|
||||
`;
|
||||
|
||||
// 添加点击效果
|
||||
item.addEventListener('click', () => {
|
||||
const scale = item.style.transform.includes('scale') ? '' : ' scale(0.98)';
|
||||
item.style.transform = `translateY(${index * this.itemHeight}px)${scale}`;
|
||||
if (scale) {
|
||||
setTimeout(() => {
|
||||
item.style.transform = `translateY(${index * this.itemHeight}px)`;
|
||||
}, 200);
|
||||
}
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
this.listContainer.classList.toggle('collapsed');
|
||||
|
||||
if (!this.isCollapsed) {
|
||||
// 展开时重新渲染
|
||||
setTimeout(() => {
|
||||
this.startIndex = -1; // 强制重新渲染
|
||||
this.endIndex = -1;
|
||||
this.render();
|
||||
}, 350);
|
||||
} else {
|
||||
// 折叠时清除内容
|
||||
this.viewport.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
updateOnScroll() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
forceRefresh() {
|
||||
this.startIndex = -1;
|
||||
this.endIndex = -1;
|
||||
this.lastRenderTime = 0;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
// 生成大量测试数据
|
||||
function generateLargeDataset(prefix, count) {
|
||||
const items = [];
|
||||
const foods = [
|
||||
'宫保鸡丁', '麻婆豆腐', '红烧肉', '鱼香肉丝', '青椒肉丝', '糖醋里脊',
|
||||
'回锅肉', '蛋炒饭', '扬州炒饭', '牛肉面', '兰州拉面', '炸酱面',
|
||||
'担担面', '热干面', '重庆小面', '小笼包', '煎饺', '蒸饺',
|
||||
'包子', '馒头', '花卷', '烧饼', '油条', '豆浆', '胡辣汤', '煎蛋',
|
||||
'培根', '香肠', '吐司', '三明治', '汉堡', '披萨', '意面', '拉面'
|
||||
];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const food = foods[i % foods.length];
|
||||
const variant = Math.floor(i / foods.length) + 1;
|
||||
const rating = '★'.repeat(Math.floor(Math.random() * 5) + 1);
|
||||
items.push(`${prefix}${food}${variant > 1 ? ` (${variant})` : ''} ${rating}`);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
// 创建数据集
|
||||
const datasets = [
|
||||
{
|
||||
title: '早餐推荐',
|
||||
data: generateLargeDataset('精品', 200),
|
||||
collapsed: false
|
||||
},
|
||||
{
|
||||
title: '主食类',
|
||||
data: generateLargeDataset('招牌', 800),
|
||||
collapsed: false
|
||||
},
|
||||
{
|
||||
title: '素食专区',
|
||||
data: generateLargeDataset('健康', 300),
|
||||
collapsed: false
|
||||
},
|
||||
{
|
||||
title: '甜品点心',
|
||||
data: generateLargeDataset('甜蜜', 500),
|
||||
collapsed: false
|
||||
},
|
||||
{
|
||||
title: '饮品推荐',
|
||||
data: generateLargeDataset('特调', 400),
|
||||
collapsed: false
|
||||
}
|
||||
];
|
||||
|
||||
// 计算总项目数
|
||||
window.totalItems = datasets.reduce((sum, d) => sum + d.data.length, 0);
|
||||
|
||||
// 初始化列表
|
||||
const listContainer = document.getElementById('listContainer');
|
||||
const virtualLists = [];
|
||||
|
||||
datasets.forEach((dataset, index) => {
|
||||
// 创建列表区域
|
||||
const section = document.createElement('div');
|
||||
section.className = 'list-section';
|
||||
|
||||
// 创建表头
|
||||
const header = document.createElement('div');
|
||||
header.className = 'list-header';
|
||||
if (dataset.collapsed) {
|
||||
header.classList.add('collapsed');
|
||||
}
|
||||
header.innerHTML = `
|
||||
<span>${dataset.title}</span>
|
||||
<span class="header-info">${dataset.data.length} 项</span>
|
||||
`;
|
||||
|
||||
section.appendChild(header);
|
||||
listContainer.appendChild(section);
|
||||
|
||||
// 创建虚拟列表
|
||||
const virtualList = new GlobalVirtualList(section, dataset.data, {
|
||||
itemHeight: 60,
|
||||
collapsed: dataset.collapsed
|
||||
});
|
||||
|
||||
virtualLists.push(virtualList);
|
||||
|
||||
// 绑定表头点击事件
|
||||
header.addEventListener('click', function() {
|
||||
virtualList.toggle();
|
||||
this.classList.toggle('collapsed');
|
||||
|
||||
// 添加点击动画
|
||||
this.style.transform = 'scale(0.98)';
|
||||
setTimeout(() => {
|
||||
this.style.transform = 'scale(1)';
|
||||
}, 200);
|
||||
|
||||
// 折叠/展开后需要重新渲染所有列表
|
||||
setTimeout(() => {
|
||||
virtualLists.forEach(list => list.forceRefresh());
|
||||
updatePerformanceInfo();
|
||||
}, 400);
|
||||
});
|
||||
});
|
||||
|
||||
// 全局滚动事件监听 - 使用节流
|
||||
const throttledGlobalRender = throttle(() => {
|
||||
virtualLists.forEach(list => list.updateOnScroll());
|
||||
updatePerformanceInfo();
|
||||
}, 32); // 约30fps,更保守的刷新率
|
||||
|
||||
window.addEventListener('scroll', throttledGlobalRender, { passive: true });
|
||||
|
||||
// 窗口大小变化时重新渲染
|
||||
window.addEventListener('resize', debounce(() => {
|
||||
virtualLists.forEach(list => list.forceRefresh());
|
||||
updatePerformanceInfo();
|
||||
}, 300));
|
||||
|
||||
// 更新性能信息
|
||||
function updatePerformanceInfo() {
|
||||
const allRendered = document.querySelectorAll('.virtual-list-item').length;
|
||||
const perfInfo = document.getElementById('performanceInfo');
|
||||
if (perfInfo) {
|
||||
perfInfo.textContent = `渲染: ${allRendered} / ${window.totalItems} 项目`;
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动提示功能
|
||||
let scrollTimer;
|
||||
const scrollIndicator = document.getElementById('scrollIndicator');
|
||||
|
||||
window.addEventListener('scroll', throttle(() => {
|
||||
scrollIndicator.classList.add('show');
|
||||
clearTimeout(scrollTimer);
|
||||
scrollTimer = setTimeout(() => {
|
||||
scrollIndicator.classList.remove('show');
|
||||
}, 1000);
|
||||
}, 100));
|
||||
|
||||
// 初始渲染
|
||||
setTimeout(() => {
|
||||
virtualLists.forEach(list => list.updateOnScroll());
|
||||
updatePerformanceInfo();
|
||||
}, 100);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user