大家好,我是 前端架构师 - 大卫。
更多优质内容请关注微信公众号 @程序员大卫。
初心为助前端人🚀,进阶路上共星辰✨,
您的点赞👍与关注❤️,是我笔耕不辍的灯💡。
功能介绍
1. 向上滚动
2. 向下滚动
3. 向左滚动
4. 向右滚动
5. 滚动暂停
单步滚动暂停
翻页滚动暂停
6. 自适应视口大小和动态数据更新
前言
先介绍下这个 NPM 包 vue-loop-scroll
的功能:
- 🔥 超大数据流畅滚动
- 即使 10 万条数据,也能丝滑滚动不卡顿!仅渲染可视区域的 2 倍数据,大幅减少 DOM 负担,让滚动更流畅。
- 🌟 适应变化,始终顺滑
- 支持容器大小动态调整,即使数据实时更新,依然能保持平滑滚动,提供最佳用户体验。
- 🔧 灵活滚动控制
- 支持四向滚动、单步停顿、滚动速度调节、鼠标悬停控制等多种配置,让滚动更符合需求。
GitHub 地址:
官网地址:
joydayx.github.io/website-vue…
NPM 包:
如果这个项目对您有帮助,欢迎留下宝贵的 ⭐Star⭐!
您的支持不仅是我们优化升级的动力,更是对开源精神的最大认可。
背景
循环滚动是一个很常见的需求,在前端开发过程中几乎不可避免。通常,我们会轮询服务端接口来更新滚动数据,而我开发这个 NPM
包的初衷是什么呢?
在介绍方案之前,我们先弄清楚两个概念:
- 可视区域:用户能看到的部分。
- 滚动区域:整个可滚动内容的范围。
只有当滚动区域大于可视区域时,才可以滚动。
传统循环滚动的弊端
在传统的循环滚动实现中,我们通常会遇到以下几个问题:
1.大数据渲染,如何优化性能?
- 直接渲染所有数据,DOM 负担过大,影响滚动流畅度。
2.数据实时更新,如何保证滚动平滑?
- 轮询服务端获取新数据时,如何避免数据更新导致的滚动卡顿或跳跃?
3.响应式页面适配,如何让滚动更顺畅?
- 在可视化大屏等自适应页面中,窗口大小变化会影响滚动区域尺寸大小,如何保证滚动不突兀?
旧的解决方案
1. 大数据渲染的处理
- 传统方案通常不做优化,直接渲染所有数据。
- 但如果数据量较大(如上千条),会导致DOM 负载过高,滚动卡顿。
2. 处理数据更新时的滚动平滑
方案 ①:根据滚动区域高度判断是否重置滚动
如果新数据在容器中所占的总高度小于当前滚动区域的最大滚动高度,则继续执行滚动; 否则,重置滚动状态,从顶部重新开始滚动。
缺点:
- 需要完整渲染所有新数据后,才能获取总高度;
- 当数据量较大时,渲染过程可能造成卡顿,影响性能与用户体验。
方案 ②:强制重置组件
通过对比新旧数据,如果不一致,则修改组件 key,强制重渲染。
缺点:
- 数据量大时,对比成本高,影响性能。
- 强制重置组件会导致滚动中断,影响用户体验。
3. 监听可视区域变化
计算当前滚动高度占滚动区域的百分比,然后用新数据在容器中所占的总高度乘以该百分比,得出新的滚动高度。
缺点:
- 需要完整渲染所有新数据来计算总高度,开销较大,影响性能。
本组件库的解决方案
1. 优化大数据渲染
初始化时,只渲染可视区域高度的两倍数据,避免一次性渲染过多内容。
当滚动超过可视区域时,删除已经滚动出视野的数据,动态加载新的数据,保持 DOM 轻量化。
如何判断加载的滚动项高度?
我们可以使用 nextTick
,通过递归不断检查加载的高度是否满足需求。
以下是主要逻辑,loadDataBatch
函数的参数包括起始索引和需要加载的高度。
loadDataBatch 是关键函数,后续还会多次使用。组件参数 loadCount 控制每次批量加载的数据量,默认值为 1。
const loadDataBatch = async (startIndex, requiredSize) => {
const loadUntilFilled = async () => {
const loadedItems = [];
let loadCount = props.loadCount;
while (loadCount-- > 0) {
startIndex++;
loadedItems.push(props.dataSource[startIndex]);
}
await nextTick();
// 计算已经加载的数据项的高度
const actualLoadedSize = calculateItemsTotalSize(loadedItems);
if (requiredSize > actualLoadedSize) {
await loadUntilFilled();
} else {
// 已经满足加载尺寸需求
}
};
await loadUntilFilled();
};
所以开始滚动前,我们只需要加载可视区域高度的 2 倍数据,代码如下:
loadDataBatch(0, viewportSize * 2);
2. 处理数据更新时的滚动平滑
这是本组件中最具挑战性、同时也是最具技巧与创意的部分。
假设当前正在滚动的项是:
[item-4, item-5, item-6, item-7, item-8, item-9]
此时,服务端返回的新数据是:
[item-xxx, item-5, item-xxx, item-xxx, item-xxx, item-xxx]
我们可以遍历当前正在滚动的旧数据,在新数据中查找与之匹配的项 item-5
,它在新数据中的索引是 1
,接着计算该项相对于可视区域上边界与下边界的距离。
匹配规则:
根据组件参数 itemKey
提供的唯一标识字段进行匹配。若未传入 itemKey
,则默认使用 JSON.stringify(item)
作为唯一标识。
itemKey
用法如下:
const dataSource = [{id: "123", value: "Hello"}, {id: "456", value: "World"} ]
<LoopScroll :dataSource itemKey="id"></LoopScroll>
然后,我们调用之前提到的 loadDataBatch
函数两次:
// 第一次调用
loadDataBatch(1, item-5 距离可视区域上边框的高度)
// 第2次调用
loadDataBatch(1, item-5 距离可视区域下边框的高度 + 可视区域的高度)
为什么第二次调用函数 loadDataBatch
需要额外加上可视区域的高度?
因为在“第一点:优化大数据渲染”中,我们约定滚动区域高度必须至少是可视区域的 2 倍,否则会导致滚动出现断层。
特殊情况:
当新数据与当前滚动数据完全没有匹配项时,意味着无法还原之前的滚动位置。
从业务角度来说,用户通常也不希望继续看到旧数据,这种情况下应当重置滚动状态,从头开始滚动,以避免误导或错乱的展示。
3. 监听可视区域变化
在解决了第二点的核心问题之后,第三点就简单多了。
当容器尺寸或列表项的高度发生变化时,我们可以假设服务端返回的是同一批数据,这意味着我们无需关心数据内容的变更,仅需重新执行第二点中的定位逻辑即可。
为了实现这一逻辑,我们可以引入一个自增的刷新 ID。每当监听到尺寸变化时,递增这个 ID,作为触发重新计算的信号。代码如下:
const updateCounter = ref(0);
const triggerUpdate = () => {
updateCounter.value++;
};
/** 监听"数据源变化"和“自增id”变化 */
watch(
() => [props.dataSource, updateCounter.value],
() => {
// 重新渲染页面数据展示
},
);
/** 监听 "可视区域内容" 尺寸变化 */
useResizeObserver(scrollViewportRef, ()=>{
triggerUpdate();
});
/** 监听 "滚动内容区域" 尺寸变化 */
useResizeObserver(scrollTrackRef, ()=>{
triggerUpdate();
});
关键技术分析
1. 为什么不使用 clientHeight 来获取高度?
我们选择使用 getBoundingClientRect()
,主要原因:
- 监听容器尺寸变化用的 API 是
ResizeObserver
,它监听到的尺寸值是带小数点的浮点数,而clientHeight
仅返回整数,会导致监听误差。 - 使用
getBoundingClientRect
返回的尺寸精度更高,能够与ResizeObserver
保持一致性。
2. 新数据到来时如何实现平滑过渡?
- 缓存旧数据,在新数据加载过程中,先缓存旧数据,按批次逐步加载新数据。每次加载后使用
nextTick
判断当前内容总高度是否超过可视区域,用以判断是否具备滚动条件。 - 如果可以滚动,恢复旧数据,并根据之前 "2.处理数据更新时的滚动平滑" 的逻辑,继续执行滚动。
- 如果不可以滚动,则停止滚动并重置所有状态。
3. 滚动暂停的处理
假设场景如下:
- 每帧滚动步长为
3px
- 每个列表项高度为
10px
在第 4 帧时,总共滚动了 3 × 4 = 12px
,超过了 10
,会导致不对齐。
解决方案:
当滚动到第 4
帧时,将滚动步长调整为 1px
,使得累计滚动距离为 3 × 3 + 1 = 10px
,这样就能对齐列表项的边界了。
4. 如何优雅地实现逆向滚动(向下和向右滚动)
假设正向滚动(向上和向左)时,我们存储的数据项信息如下:
[itemInfo-1, itemInfo-2, itemInfo-3]
其中 itemInfo
主要包含 height
、top
、bottom
等信息,用于计算滚动位置。
逆向滚动的关键点:
- 存储数据项的顺序保持不变,还是
[itemInfo-1, itemInfo-2, itemInfo-3]
。 - 而渲染数据时倒序排列,确保滚动逻辑清晰,避免混乱。
5. CSS 的特殊设置
1. CSS 设置 content: ""
- 当向左滚动时,在内容前插入 content: "";
- 当向右滚动时,在内容后插入 content: ""。
这样处理是为了解决使用 flex
布局中的 gap
属性可能引发的抖动问题。由于 gap
仅作用于相邻项之间,当滚动时第 2 项变为第 1 项,前方缺少相邻元素,导致布局位置略有偏移,从而出现抖动现象。
2. CSS 设置 display: flow-root
滚动区域
设置 display: flow-root;
主要是为了创建一个 BFC(块格式化上下文),从而解决浮动塌陷和 margin
合并问题。
例如:每个子项使用 margin: 10px 0
作为垂直间距时,滚动区域的总高度会出现误差,因为第一项的上边距和最后一项的下边距不会被包含在父元素(滚动区域)的高度计算中。而设置 display: flow-root
可以创建一个 BFC,从而避免这种 margin
折叠问题,确保高度计算准确。
如果浏览器不支持 display: flow-root;
,可以使用以下替代方案:
.scroll-loop-track{
/* display: flow-root; */
&::before,
&::after {
content: "";
display: table;
}
}
3. CSS 设置 width: max-content
在横向(向左或向右)滚动时,将滚动区域
设置为 width: max-content
,可以让其宽度根据内容的最大固有宽度自动扩展,避免内容换行。
注意事项
在实际项目中使用时,请合理设置 itemKey 和 loadCount,以获得最佳性能和正确的数据更新。
-
itemKey 是列表数据项的唯一标识字段名。在数据更新时,组件会通过
itemKey
找到对应的项并更新其内容,以确保正确的数据匹配和渲染。 -
loadCount 控制每次批量加载的数据量,默认值为
1
。建议设置为当前可视区域最多能展示的项数,这样可以减少不必要的渲染计算,提升滚动性能,而不是每次仅加载1
项后再判断是否填满可视区域。
总结
本组件专为大数据滚动场景设计,滚动超顺滑,只渲染可见内容,性能杠杠的。
还能自动适应尺寸和数据变化,支持四向滚动、速度调节等各种控制,灵活又好用。
最后
如果觉得本文对你有帮助,欢迎点赞👍、关注➕、收藏❤️,也欢迎在评论区交流你的看法!