🔥 最牛的 Vue3 无限循环滚动 NPM 包开源了!

大家好,我是 前端架构师 - 大卫

更多优质内容请关注微信公众号 @前端大卫

初心为助前端人🚀,进阶路上共星辰✨,

您的点赞👍与关注❤️,是我笔耕不辍的灯💡。

前言

先介绍下这个 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();
});

难点分析

不能简单地对比可视区域clientHeightscrollHeight,而是要对比可视区域高度滚动区域高度

此外,获取高度时不能使用 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 里主要存储 heighttopbottom 等信息。

逆向滚动的关键点

  • 存储数据项的顺序保持不变(不要倒序存储)。
  • 渲染时倒序排列,确保滚动逻辑清晰,不容易混乱。

注意事项

在实际项目中使用时,请合理设置 itemKeyloadCount,以获得最佳性能和正确的数据更新。

  • itemKey 是列表数据项的唯一标识字段名。在数据更新时,组件会通过 itemKey 找到对应的项并更新其内容,以确保正确的数据匹配和渲染。

  • loadCount 控制每次批量加载的数据量,默认值为 1。建议设置为当前可视区域最多能展示的项数 ,这样可以减少不必要的渲染计算,提升滚动性能,而不是每次仅加载 1 项后再判断是否填满可视区域。

总结

这个 NPM 包通过监听滚动区域尺寸变化、精确匹配新旧数据、动态调整滚动位置,成功解决了虚拟列表滚动同步问题。同时,优化了性能,避免了不必要的渲染,提升了用户体验。

这套方案适用于长列表数据更新、动态高度列表、数据实时变化的场景,在实际项目中能显著提升滚动流畅度和稳定性。

最后

如果觉得本文对你有帮助,欢迎点赞👍、关注➕、收藏❤️,也欢迎在评论区交流你的看法!

相关推荐
Lepusarcticus几秒前
《掌握 JavaScript 字符串操作,这一篇就够了!》
前端·javascript
田本初5 分钟前
vue-cli工具build测试与生产包对css处理的不同
前端·css·vue.js
Enti7c25 分钟前
Cookie可以存哪些指?
javascript
inxunoffice1 小时前
批量在多个 PDF 的指定位置插入页,如插入封面、插入尾页
前端·pdf
木木黄木木1 小时前
HTML5 Canvas绘画板项目实战:打造一个功能丰富的在线画板
前端·html·html5
ElasticPDF_新国产PDF编辑器1 小时前
React 项目 PDF 批注插件库在线版 API 示例教程
javascript
豆芽8191 小时前
基于Web的交互式智能成绩管理系统设计
前端·python·信息可视化·数据分析·交互·web·数据可视化
不是鱼1 小时前
XSS 和 CSRF 为什么值得你的关注?
前端·javascript
顺遂时光1 小时前
微信小程序——解构赋值与普通赋值
前端·javascript·vue.js
anyeyongzhou1 小时前
img标签请求浏览器资源携带请求头
前端·vue.js