大家好,我是 前端架构师 - 大卫。
更多优质内容请关注微信公众号 @前端大卫。
初心为助前端人🚀,进阶路上共星辰✨,
您的点赞👍与关注❤️,是我笔耕不辍的灯💡。
前言
先介绍下这个 NPM 包 vue-loop-scroll
的功能:
- 🔥 超大数据流畅滚动
- 即使 10 万条数据,也能丝滑滚动不卡顿!仅渲染可视区域的 2 倍数据,大幅减少 DOM 负担,让滚动更流畅。
- 🌟 适应变化,始终顺滑
- 支持容器大小动态调整,即使数据实时更新,依然能保持平滑滚动,提供最佳用户体验。
- 🔧 灵活滚动控制
- 支持四向滚动 、单步停顿 、滚动速度调节 、鼠标悬停控制等多种配置,让滚动更符合需求。
npm 包地址: www.npmjs.com/package/@jo...
官网地址: joydayx.github.io/website-vue...
如果觉得这个项目对你有帮助,欢迎留下宝贵的 ⭐Star⭐!你的支持不仅是我们持续优化的动力,也是对开源精神的最大认可和鼓励。
背景
循环滚动 是一个很常见的需求,在前端开发过程中几乎不可避免。通常,我们会轮询服务端接口来更新滚动数据,而我开发这个 NPM
包的初衷是什么呢?
在介绍方案之前,我们先弄清楚两个概念:
- 可视区域:用户能看到的部分。
- 滚动区域:整个可滚动内容的范围。
只有当滚动区域大于可视区域时,才可以滚动。
传统循环滚动的弊端
在传统的循环滚动实现中,我们通常会遇到以下几个问题:
1.大数据渲染,如何优化性能?
- 直接渲染所有数据,DOM 负担过大,影响滚动流畅度。
2.数据实时更新,如何保证滚动平滑?
- 轮询服务端获取新数据时,如何避免数据更新导致的滚动卡顿或跳跃?
3.响应式页面适配,如何让滚动更顺畅?
- 在可视化大屏等自适应页面中,窗口大小变化会影响滚动区域尺寸大小,如何保证滚动不突兀?
传统循环滚动的解决方案
1.大数据渲染的处理
- 传统方案通常不做优化,直接渲染所有数据。
- 但如果数据量较大(如上千条),会导致DOM 负载过高,滚动卡顿。
2.处理数据更新时的滚动平滑
方案 ①:根据滚动区域高度判断是否重置滚动
- 如果新数据的滚动区域高度小于当前滚动区域,则继续滚动。
- 如果高度超过当前滚动区域,则重置滚动状态,从头开始滚动。
方案 ②:强制重置组件
通过对比新旧数据,如果不一致,则修改组件 key,强制重渲染。缺点:
- 数据量大时,对比成本高,影响性能。
- 强制重置组件会导致滚动中断,影响用户体验。
3.适应页面尺寸变化
- 监听可视区域变化,计算当前滚动高度占滚动区域的百分比,然后用新数据的滚动区域高度乘以该百分比,得出新的滚动高度。
- 但这种方法需要渲染所有数据来计算总高度,开销较大,影响性能,因此也不是最佳方案。
新的解决方案
1. 优化大数据渲染:只渲染可视区域 2 倍的数据
初始化 时,只渲染可视区域高度的两倍数据,避免一次性渲染过多内容。
当滚动超过可视区域 时,删除已经滚动出视野的数据,动态加载新的数据,保持 DOM 轻量化。
如何判断加载的滚动项高度?
我们可以使用 nextTick
,通过递归不断检查加载的高度是否满足需求。
以下是主要逻辑,loadDataBatch
函数的参数包括起始索引 和需要加载的高度。
loadDataBatch 是关键函数,后续还会多次使用。组件参数 loadCount 控制每次批量加载的数据量,默认值为 1。
js
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 倍数据,代码如下:
js
loadDataBatch(0, viewportSize * 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
,然后计算 item-5
距离可视区域上边框和下边框的距离。
匹配的依据 :通过组件参数 itemKey
,即列表项的唯一标识字段名。如果未传递 itemKey
,则默认使用 JSON.stringify(item)
作为唯一标识。itemKey
的使用例子如下:
js
const dataSource = [{id: "123", value: "Hello"}, {id: "456", value: "World"} ]
<LoopScroll :dataSource itemKey="id"></LoopScroll>
然后,我们调用 loadDataBatch
两次:
js
// 第一次调用
loadDataBatch(1, item-5 距离可视区域上边框的高度)
// 第2次调用
loadDataBatch(1, item-5 距离可视区域下边框的高度 + 可视区域的高度)
为什么第二次调用需要额外加上可视区域的高度?
因为在第一点解决方案中,我们规定滚动区域的高度必须是可视区域的 2 倍,否则滚动会有问题。
特殊情况: 如果新数据和当前滚动的数据完全不匹配 ,那么从业务角度来看,用户一般不希望看到旧数据,此时我们就需要重置滚动状态 ,从头开始滚动。
第三点的解决方案
在解决了第二点后,第三点就简单了。
当容器尺寸或滚动项高度发生变化时,我们只需要假设此时服务端返回了一模一样的数据,然后重新执行第二点解决方案的逻辑即可。
代码实现上,我们可以定义一个自增 ID ,在监听到尺寸变化时,刷新这个ID,触发重新计算。代码逻辑如下:
js
const updateCounter = ref(0);
const triggerUpdate = () => {
updateCounter.value++;
};
/** 监听"数据源变化"和"自增id"变化 */
watch(
() => [props.dataSource, updateCounter.value],
() => {
// 重新渲染页面数据展示
},
);
/** 监听 "可视区域内容" 尺寸变化 */
useResizeObserver(scrollViewportRef, ()=>{
triggerUpdate();
});
/** 监听 "滚动内容区域" 尺寸变化 */
useResizeObserver(scrollTrackRef, ()=>{
triggerUpdate();
});
难点分析
不能简单地对比可视区域 的 clientHeight
和 scrollHeight
,而是要对比可视区域高度 和滚动区域高度。
此外,获取高度时不能使用 clientHeight
,而应该使用 getBoundingClientRect
,原因如下:
- 监听容器尺寸变化用的 API 是 ResizeObserver ,它返回的是带小数点的尺寸,而
clientHeight
只返回整数,可能导致监听误差。 - 使用
getBoundingClientRect
可确保计算结果更精准。
2. 新数据到来时如何平滑过渡?
- 先缓存旧数据 ,按批次循环加载新数据,并使用
nextTick
判断当前高度是否超过可视区域,以此判断是否可滚动。 - 如果可以滚动 ,则恢复旧数据,并按照第二点解决方案让其继续滚动。
- 如果无法滚动,则停止滚动并重置所有状态。
3. 单步滚动停顿的处理
假设:
- 每帧滚动高度 =
3
- 单个滚动项高度 =
10
那么滚动到第 4
帧时,滚动了 3 × 4 = 12
,此时超过了 10
,会导致不对齐。
解决方案:
当滚动到第 4
帧时,将滚动高度调整为 1
,使得总滚动距离为 3 × 3 + 1 = 10
,这样就能对齐滚动项高度。
4. 如何优雅地实现逆向滚动?
假设正向滚动时,我们存储的数据项信息如下:
js
[itemInfo-1, itemInfo-2, itemInfo-3]
其中 itemInfo
里主要存储 height
、top
、bottom
等信息。
逆向滚动的关键点:
- 存储数据项的顺序保持不变(不要倒序存储)。
- 渲染时倒序排列,确保滚动逻辑清晰,不容易混乱。
注意事项
在实际项目中使用时,请合理设置 itemKey 和 loadCount,以获得最佳性能和正确的数据更新。
-
itemKey 是列表数据项的唯一标识字段名。在数据更新时,组件会通过
itemKey
找到对应的项并更新其内容,以确保正确的数据匹配和渲染。 -
loadCount 控制每次批量加载的数据量,默认值为
1
。建议设置为当前可视区域最多能展示的项数 ,这样可以减少不必要的渲染计算,提升滚动性能,而不是每次仅加载1
项后再判断是否填满可视区域。
总结
这个 NPM
包通过监听滚动区域尺寸变化、精确匹配新旧数据、动态调整滚动位置,成功解决了虚拟列表滚动同步问题。同时,优化了性能,避免了不必要的渲染,提升了用户体验。
这套方案适用于长列表数据更新、动态高度列表、数据实时变化的场景,在实际项目中能显著提升滚动流畅度和稳定性。
最后
如果觉得本文对你有帮助,欢迎点赞👍、关注➕、收藏❤️,也欢迎在评论区交流你的看法!