Skip to content

cooolinx/fast-scrollview.js

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FastScrollView

⚡ 一个高性能的虚拟滚动库,采用跳跃式按需渲染策略,专为处理海量数据设计。

✨ 核心特性

  • 跳跃式渲染 - 可以从任意位置开始渲染,无需预先计算
  • 🚀 零跳动 - 精心设计的占位符管理,滚动如丝般顺滑
  • 🔄 双向扩展 - 向上或向下滚动时自动追加新元素
  • 🎯 原生 JavaScript - 零依赖,仅 10 KB(gzip 后更小)
  • 💪 海量数据 - 轻松处理 100万+ 条数据
  • 🔧 丰富的 API - 完整的数据操作和滚动控制方法
  • 📱 移动端友好 - 完美支持触摸滚动

🎯 跳跃式渲染策略

FastScrollView 采用创新的跳跃式按需渲染策略,与传统虚拟滚动有本质区别:

传统虚拟滚动的问题

  • ❌ 需要预先计算所有元素的位置
  • ❌ 初始化时需要遍历所有数据
  • ❌ 跳转到 #5000 时,需要渲染 #0-#5000 所有元素
  • ❌ 频繁调整 DOM 导致跳动
  • ❌ 对动态高度支持不够友好

FastScrollView 的优势

  • 无需预先计算 - 不遍历所有元素,直接从目标位置开始
  • 跳跃式跳转 - 跳转到 #5000 时,只渲染 #5000 附近 20-30 个元素
  • 双向扩展 - 向上/向下滚动时自动追加,不删除已有元素
  • 零跳动 - 不调整 scrollTop,完全依赖占位符
  • 更快初始化 - 只渲染首屏,启动几乎瞬间完成

工作原理

场景:10000 条数据,跳转到 #5000

┌─────────────────────────────────┐
│  topSpacer (250,000px)          │  ← #0-#4999 未渲染(占位符)
├─────────────────────────────────┤
│  ┌───────────────────────────┐  │
│  │ Item #5000 (已渲染)       │  │
│  │ Item #5001 (已渲染)       │  │  ← 仅渲染可见区域
│  │ ...                       │  │     约 20-30 个元素
│  │ Item #5020 (已渲染)       │  │
│  └───────────────────────────┘  │
├─────────────────────────────────┤
│  bottomSpacer (249,000px)       │  ← #5021-#9999 未渲染(占位符)
└─────────────────────────────────┘

结果:
✅ DOM 中只有 20 个元素
✅ 滚动条显示完整列表长度
✅ 可以继续向上或向下滚动
✅ 滚动时自动扩展渲染范围

性能对比

操作 传统虚拟滚动 FastScrollView
初始化 10万数据 ~500ms(计算位置) ~10ms(渲染首屏)
跳转到 #5000 渲染 5000+ 元素 渲染 20-30 元素
DOM 节点数 5000+ 20-30
滚动跳动 可能跳动 完全无跳动 ✅

📦 安装

NPM

npm install fast-scrollview

CDN

<script src="https://unpkg.com/fast-scrollview/dist/fast-scrollview.min.js"></script>

本地构建

# 克隆仓库
git clone git@github.com:cooolinx/fast-scrollview.js.git
cd fast-scrollview

# 安装依赖
npm install

# 构建
npm run build

🚀 快速开始

HTML 结构

<div id="scroll-container" style="height: 600px;"></div>

JavaScript 使用

// 1. 准备数据(可以是任意大小的数组)
const items = [];
for (let i = 0; i < 100000; i++) {
  items.push({
    id: i,
    title: `数据项 ${i}`,
    content: `这是第 ${i} 条数据`
  });
}

// 2. 定义渲染函数
function renderItem(item, index, totalSize) {
  const div = document.createElement('div');
  div.className = 'list-item';
  div.innerHTML = `
    <strong>#${index}</strong>
    <span>${item.title}</span>
    <p>${item.content}</p>
  `;
  return div;
}

// 3. 创建 FastScrollView 实例
const fsv = new FastScrollView(
  '#scroll-container',  // 容器元素或选择器
  items,                // 数据数组
  renderItem,           // 渲染函数
  {
    bufferThreshold: 2,   // 缓冲阈值(可选,默认 2)
    align: 'top',         // 对齐方式(可选,'top' 或 'bottom',默认 'top')
    onScroll: (info) => { // 滚动回调(可选)
      console.log('滚动中...', info);
    }
  }
);

// 4. 使用跳跃式渲染
fsv.scrollToItem(50000);  // 跳转到 #50000,只渲染附近元素!

📚 API 文档

构造函数

new FastScrollView(container, items, render, options)

参数

  • container (HTMLElement | string) - 容器元素或 CSS 选择器
  • items (Array) - 要渲染的数据数组
  • render (Function) - 渲染函数,签名:(item, index, totalSize) => HTMLElement | string
  • options (Object, 可选) - 配置选项
    • bufferThreshold (number) - 缓冲阈值,默认 2(表示提前2个屏幕高度触发渲染)
    • align (string) - 对齐方式,'top'(默认)或 'bottom'(底部对齐)
    • onScroll (Function) - 滚动时的回调函数

数据操作方法

setItems(items)

设置新的数据数组,会清除所有高度缓存。

fsv.setItems(newItemsArray);

setItem(index, item)

更新指定索引的数据项。

fsv.setItem(10, { id: 10, title: '更新后的标题' });

insertItem(index, item)

在指定位置插入数据项。

fsv.insertItem(5, { id: 999, title: '插入的数据' });

append(item)

在数组末尾添加数据项。

fsv.append({ id: 1001, title: '新数据' });

prepend(item)

在数组开头添加数据项。

fsv.prepend({ id: 1002, title: '最新数据' });

remove(itemOrIndex)

删除数据项,可以传入数据项本身或索引。

// 通过索引删除
fsv.remove(5);

// 通过数据项删除
fsv.remove(item);

滚动控制方法

scrollToItem(itemOrIndex)

跳转到指定的数据项(核心方法),支持跳跃式渲染。

// 通过索引跳转 - 只渲染该位置附近的元素
fsv.scrollToItem(5000);  // 跳转到 #5000,只渲染 #5000 附近 20-30 个元素

// 通过数据项跳转
fsv.scrollToItem(item);

特点:

  • ✅ 跳跃式渲染:只渲染目标位置附近的元素
  • ✅ 不会全量渲染:即使跳转到 #50000 也只渲染附近内容
  • ✅ 智能优化:如果目标 item 已渲染,直接滚动不重新渲染(性能更优)
  • ✅ 基于缓存计算:已访问过的位置使用精确的缓存高度

scrollToTop()

跳转到顶部。

fsv.scrollToTop();  // 等同于 fsv.scrollToItem(0)

scrollToBottom()

跳转到底部。

fsv.scrollToBottom();  // 等同于 fsv.scrollToItem(items.length - 1)

注意: scrollToTop()scrollToBottom() 也是按需渲染,不会加载整个列表。

获取信息方法

getVisibleRange()

获取当前可视区域的信息。

const range = fsv.getVisibleRange();
// 返回: { start: 10, end: 30, count: 20 }

isAtScrollBottom(threshold)

判断是否滚动到底部(用于聊天应用场景)。

const isAtBottom = fsv.isAtScrollBottom();  // 默认容差 10px
const isAtBottom2 = fsv.isAtScrollBottom(5); // 自定义容差 5px

参数:

  • threshold (number, 可选) - 容差值(像素),默认为 10px

返回值:

  • boolean - 是否在底部

使用场景:

在聊天应用中,当用户正在查看历史消息时,新消息到来不应该自动滚动;只有当用户在底部时,才应该自动滚动到新消息。

// 智能自动滚动示例
function addNewMessage(message) {
  // 添加消息前,先检查是否在底部
  const wasAtBottom = chatView.isAtScrollBottom();
  
  // 添加新消息
  chatView.append(message);
  
  // 只有之前在底部时,才自动滚动
  if (wasAtBottom) {
    chatView.scrollToBottom();
  }
}

其他方法

refresh()

刷新显示,重新测量所有元素的高度。

fsv.refresh();

destroy()

销毁实例,清理事件监听器和数据。

fsv.destroy();

🎯 底部对齐模式(聊天应用专用)

FastScrollView 支持 align: 'bottom' 参数,特别适合聊天应用等需要从底部显示内容的场景。

使用方法

const chatView = new FastScrollView(
  '#chat-container',
  messages,
  renderMessage,
  {
    align: 'bottom',  // 👈 启用底部对齐
    bufferThreshold: 2
  }
);

特性

  • 自动贴底 - 当消息数量少时,自动贴在底部显示(像真实的聊天应用)
  • 智能定位 - setItems() 后自动滚动到最底部
  • 完美体验 - 无论内容多少,始终从底部开始显示

效果对比

// 默认模式 (align: 'top')
// 1-2 条消息时,从顶部开始显示
┌─────────────────┐
 消息1           
 消息2           
                 
     (空白)      
                 
└─────────────────┘

// 底部对齐模式 (align: 'bottom')
// 1-2 条消息时,贴在底部显示
┌─────────────────┐
                 
     (空白)      
                 
 消息1           
 消息2           
└─────────────────┘

适用场景

  • 💬 聊天应用
  • 📱 消息列表
  • 💭 评论区
  • 📝 日志查看器

🎨 示例

项目包含多个完整的示例,演示不同的使用场景:

1. 基础示例 (examples/basic.html)

展示 10万条数据的基本用法,包括所有 API 方法的演示。

  • ✅ 10万条数据流畅滚动
  • ✅ 完整的操作按钮演示
  • ✅ 实时性能监控

2. 动态高度示例 (examples/dynamic-height.html)

展示不同高度的卡片元素,验证动态高度测量功能。

  • ✅ 2万条不同高度的卡片
  • ✅ 自动高度测量和缓存
  • ✅ 准确的滚动定位

3. 聊天应用示例 (examples/chat-app.html)

模拟真实的聊天应用场景。

  • ✅ 5万条聊天消息
  • ✅ 实时发送消息
  • ✅ 完整的用户交互

4. 底部对齐测试 (examples/bottom-align-test.html)

测试 align: 'bottom' 参数的各种场景。

  • ✅ 测试 1-2 条消息贴底显示
  • ✅ 测试多条消息滚动到底部
  • ✅ 与默认 top 对齐模式对比

5. 聊天自动滚动 (examples/chat-auto-scroll.html)

演示 isAtScrollBottom() 方法的实际应用。

  • ✅ 智能判断是否需要自动滚动
  • ✅ 对比智能滚动和强制滚动的体验差异
  • ✅ 实时消息流演示

6. scrollToItem 性能测试 (examples/scroll-performance-test.html)

测试优化后的 scrollToItem() 方法性能。

  • ✅ 演示已渲染 item 的直接滚动(无需重新渲染)
  • ✅ 实时性能指标显示
  • ✅ 对比跳跃渲染和直接滚动的性能差异

查看示例

# 构建项目
npm run build

# 使用浏览器打开
open examples/index.html

或者使用本地服务器:

npx http-server -p 8080
# 然后访问 http://localhost:8080/examples/

🎯 使用场景

FastScrollView 适用于以下场景:

  • 📋 长列表 - 电商商品列表、搜索结果等
  • 💬 聊天应用 - 历史消息记录、群聊记录
  • 📊 数据表格 - 大量数据的表格展示
  • 📱 社交媒体 - 时间线、动态列表
  • 📁 文件管理器 - 大量文件/文件夹列表
  • 📝 日志查看器 - 系统日志、应用日志

⚙️ 工作原理详解

跳跃式渲染流程

  1. 初始化 - 从 scrollTop=0 开始,只渲染首屏内容(约 20-30 个元素)
  2. 向下滚动 - 检测滚动到接近已渲染区域底部时,自动追加新元素
  3. 向上滚动 - 检测滚动到接近已渲染区域顶部时,自动在前面插入元素
  4. 跳跃跳转 - 清空现有渲染,从目标位置重新开始渲染
  5. 占位符管理 - topSpacer 和 bottomSpacer 代表未渲染区域的高度

双向扩展机制

// 向下扩展:当滚动到接近底部时
if (scrollBottom + threshold > renderedBottom) {
  appendItems(...);  // 追加新元素
}

// 向上扩展:当滚动到接近顶部时
if (scrollTop - threshold < renderedTop) {
  prependItems(...);  // 在前面插入元素
}

性能数据

数据量 传统渲染 FastScrollView DOM 节点比
1,000 1,000 个 ~20 个 50x ⬇️
10,000 10,000 个 ~20 个 500x ⬇️
100,000 100,000 个 ~20 个 5000x ⬇️

🔧 高级配置

自定义预估高度

设置合适的预估高度可以提升位置计算的准确性(特别是在跳转时):

const fsv = new FastScrollView(container, items, render, {
  estimatedItemHeight: 100  // 设置为元素的平均高度
});

建议:

  • 统一高度的列表:设置为实际高度
  • 动态高度的列表:设置为平均高度
  • 高度差异大的列表:设置为中位数高度

调整缓冲阈值

控制何时触发新元素的渲染:

const fsv = new FastScrollView(container, items, render, {
  bufferThreshold: 3  // 提前 3 个屏幕高度触发渲染(默认 2)
});

说明:

  • 值越大,渲染的元素越多,滚动越流畅,但内存占用越高
  • 值越小,渲染的元素越少,内存占用越低,但快速滚动可能出现空白
  • 默认值 2 是平衡性能和体验的最佳值

滚动事件监听

const fsv = new FastScrollView(container, items, render, {
  onScroll: (info) => {
    console.log('可视范围:', info.visibleStart, '-', info.visibleEnd);
    console.log('滚动位置:', info.scrollTop);
    
    // 无限滚动加载
    if (info.visibleEnd >= items.length - 10) {
      loadMoreData();
    }
  }
});

📝 技术细节

跳跃式渲染的核心机制

1. 渲染范围追踪

this.renderedStartIndex  // 已渲染区域的起始索引
this.renderedEndIndex    // 已渲染区域的结束索引

2. 高度缓存

this.renderedCache.set(index, { height })  // 缓存已测量的高度

3. 占位符计算

topSpacer.height = sum(heights[0 ~ renderedStartIndex])
bottomSpacer.height = sum(heights[renderedEndIndex ~ items.length])

渲染策略示例

// 场景:10000 条数据

// 初始化
fsv = new FastScrollView(...)
// → 渲染 #0-#20
// → renderedStartIndex = 0, renderedEndIndex = 20

// 跳转到 #5000
fsv.scrollToItem(5000)
// → 清空现有渲染
// → 渲染 #5000-#5020
// → renderedStartIndex = 5000, renderedEndIndex = 5020
// → topSpacer = 5000 * 50px = 250,000px
// → bottomSpacer = 4980 * 50px = 249,000px

// 向下滚动
// → 自动追加 #5021-#5040
// → renderedEndIndex = 5040
// → bottomSpacer 减少

// 向上滚动
// → 自动在前面插入 #4980-#4999
// → renderedStartIndex = 4980
// → topSpacer 减少

为什么不会跳动?

  1. 不调整 scrollTop - 绝不在用户滚动时修改滚动位置
  2. 只扩展不删除 - 已渲染的元素保留在 DOM 中
  3. 占位符稳定 - 只在扩展渲染范围时更新占位符
  4. 测量后不补偿 - 新元素测量完高度后,不调整位置

🐛 常见问题

1. 滚动位置不准确?

原因: 元素高度变化但没有重新测量。

解决: 调用 refresh() 方法重新测量所有高度。

fsv.refresh();

2. 跳转后位置略有偏差?

原因: 使用估算高度计算位置,未测量过的元素用 estimatedItemHeight

解决:

  • 设置更准确的预估高度
  • 第二次跳转到同一位置会更准确(因为已有缓存)
const fsv = new FastScrollView(container, items, render, {
  estimatedItemHeight: 80  // 调整为实际平均高度
});

3. 快速滚动时出现空白?

原因: 缓冲阈值太小,来不及渲染新元素。

解决: 增加缓冲阈值。

const fsv = new FastScrollView(container, items, render, {
  bufferThreshold: 3  // 提前 3 个屏幕高度触发渲染
});

4. 动态内容更新后显示异常?

原因: 内容变化导致高度变化。

解决: 使用 setItem() 方法,会自动清除缓存并更新。

fsv.setItem(index, newItem);  // 自动清除该项的高度缓存

💡 最佳实践

1. 跳转优化

// ✅ 推荐:使用 scrollToItem 跳转
fsv.scrollToItem(5000);  // 只渲染附近元素

// ❌ 不推荐:遍历到目标位置
for (let i = 0; i < 5000; i++) {
  // 会渲染所有中间元素
}

2. 批量操作

// ✅ 推荐:使用批量方法
fsv.beginUpdate();
items.forEach(item => fsv.items.push(item));
fsv.endUpdate();

// ❌ 不推荐:逐个操作
items.forEach(item => fsv.append(item));  // 每次都会重新渲染

3. 高度估算

// ✅ 准确的预估高度
const avgHeight = items.reduce((sum, item) => sum + measureHeight(item), 0) / items.length;
const fsv = new FastScrollView(container, items, render, {
  estimatedItemHeight: avgHeight
});

// ⚠️ 随意设置可能导致跳转位置偏差

4. 内存管理

// 长期运行的应用,定期清理缓存
setInterval(() => {
  if (fsv.renderedCache.size > 1000) {
    fsv.refresh();  // 清理缓存,重新开始
  }
}, 60000);

5. 响应式更新

// 窗口大小变化时刷新
window.addEventListener('resize', () => {
  fsv.refresh();
});

📄 浏览器支持

  • ✅ Chrome (最新版)
  • ✅ Firefox (最新版)
  • ✅ Safari (最新版)
  • ✅ Edge (最新版)
  • ✅ iOS Safari
  • ✅ Android Chrome

最低要求: 支持 ES6 的现代浏览器

📜 开源协议

MIT License

🤝 贡献

欢迎提交 Issue 和 Pull Request!

📮 反馈

如有问题或建议,请提交 Issue 或联系作者。


Happy Coding! 🎉

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published