Vue3封装自动滚动列表指令(含网页缩放滚动问题)

 更新时间:2023年05月23日 10:47:11   作者:xzxldl  
本文主要介绍了Vue3封装自动滚动列表指令(含网页缩放滚动问题),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

长列表自动滚动跟banner自动切换一样,是一个C端展示页面经常遇到的需求。不过网上各类组件库基本都对“幻灯片”(banner使用的组件)组件有封装,自带好自动切换的功能,而长列表(或表格等)自动滚动的功能则涉及甚少。

这里分享一个我项目中封装的自动滚动指令,且附带期间解决了页面缩放导致滚动失效的解决思路与方案:

需求定义

首先,我们需要理清这个通用指令的需求点,方便进行下一步设计。这里需要的需求点有:

  • 支持任意组件/标签的滚动。
  • 可控制滚动速度。
  • 可设置鼠标移入后停止滚动。

大致思路设计

首先,满足第一点,即指令可以在任意组件(包括原生的与自己封装的Vue组件)上使用,考虑到滚动相关的API,初步确定,我们需要使用到:

scrollTo, scrollHeight, clientHeight

分别用来设置滚动高度,获取最大滚动高度 与 可视区域高度。

这一点很好满足,任何通过我们上述的组件获取到的DOM(HTMLElement)都支持这三个接口/属性。

其次是速度控制,使用数字来控制每帧滚动的距离(px),我们可以将速度作为指令的参数。

最后是可选的鼠标移入暂停功能,boolean类型的选项,很容易就能想到使用指令修饰符。

综上,我们的指令调用方式需要满足以下:

<element v-scroll.mouse="1.5"></element>

编码实现

1. 指令注册和整体“架构”代码

/**
 * main.ts
 */
import scrollDirective from '@/directive/scroll.ts';
...
const app = createApp(App);
// 注册指令
app.directive(scrollDirective.name, scrollDirective);
...
/**
 * scroll.ts
 */
 import { DirectiveBinding } from 'vue';
 export default {
     name: 'scroll',
     mounted: (el: HTMLElement, binding: DirectiveBinding) => {},
     unmounted: (el: HTMLElement, binding: DirectiveBinding) => {}
 }

2. 速度控制以及滚动

JS编写动画,本能的就想到RAF能够最好的实现动画效果,RAF并在性能与视觉效果之间自动做好权衡,就是你了。

将速度speed作为指令参数,在动画函数中,用当前 scrollTop 累加 speed 来实现滚动效果。

/**
 * scroll.ts 
 */ 
import { DirectiveBinding } from 'vue';
// 我们会在DOM上拓展一些属性用于滚动动画的执行,这里拓展一下类型,方便编码
interface AnimationElement extends HTMLElement {
    speed: number;
}
const DEFAULT_SPEED = 1;
const RAF = window.requestAnimationFrame;
const CancelRAF = window.cancelAnimationFrame;
const elementScroll = (el: AnimationElement) => {
    const maxScrollTop = el.scrollHeight - el.clientHeight;
    // 根据当前滚动高度与滚动速度,计算出新的滚动高度
    const scrollTop =
        el.scrollTop + el.speed >= maxScrollTop // 超过最大滚动高度重置 --〉 从头再来
            ? 0
            : el.scrollTop + el.speed;
    // 执行滚动
    el.scrollTo({
        top: scrollTop
    });
    // 继续执行下一帧动画
    RAF(elementScroll.bind(null, el));
}
export default {
    name: 'scroll',
    mounted: (el: AnimationElement, binding: DirectiveBinding) => {
        const maxScrollTop = el.scrollHeight - el.clientHeight;
        // 没有滚动空间的时候,无需滚动,直接返回。
        if (maxScrollTop <= 0) {
            return;
        }
        // 将速度变量存到DOM中,方便后续动画函数取用
        el.speed = binding.value || DEFAULT_SPEED;
        // 使用RAF调用动画函数
        RAF(elementScroll.bind(null, el));
    },
    unmounted: (el: HTMLElement, binding: DirectiveBinding) => {}
}

我们随便写一个列表来试试

<template>
    <ul v-scroll class="w-[300px] h-[400px] overflow-auto bg-[darkcyan]">
        <li>1</li>
        <li>2</li>
        ...
    </ul>
</template>

看看效果,perfect!

scroll.gif

可以修改一下速度,让他滚快点儿

<ul v-scroll="2" ...>

Nice,没毛病。

scrollFast.gif

3. 鼠标移入暂停滚动,移出恢复滚动

要实现这个功能有两个要点:

一是事件监听,鼠标移入/移出容器时,将动画暂停/重启;

二是获取到当前容器滚动动画id(RAF 返回的),鼠标移入时,使用 window.cancelAnimationFrame 暂停动画。

/**
 * scroll.ts 
 */ 
interface AnimationElement extends HTMLElement {
    speed: number;
    animationId: number;
}
...
const elementScroll = (el: AnimationElement) => {
    ...
    // 继续执行下一帧,并更新动画id
    el.animationId = RAF(elementScroll.bind(null, el));
}
// 鼠标移入暂停
const mouseEnterHandler = (el: AnimationElement) => {
    if (el.animationId) {
        // 取消动画
        CancelRAF(el.animationId);
        el.animationId = undefined;
    }
};
// 鼠标移出继续运行动画
const mouseLeaveHandler = (el: AnimationElement) =>
    (el.animationId = RAF(scrollElement.bind(null, el)));
export default {
    name: 'scroll',
    mounted: (el: AnimationElement, binding: DirectiveBinding) => {
        ...
        // 修改原来初始化运行动画的语句,将RAF结果存到el中,方便暂停动画时使用
        el.animationId = RAF(scrollElement.bind(null, el));
        // 检测是否传递修饰符,传递了监听鼠标移入移出动画
        if (binding.modifiers.mouse) {
            el.addEventListener('mouseenter', mouseEnterHandler.bind(null, el));
            el.addEventListener('mouseleave', mouseLeaveHandler.bind(null, el));
        }
    },
    unmounted: (el: HTMLElement, binding: DirectiveBinding) => {
        // 别忘了DOM解绑时解除事件监听
        if (binding.modifiers.mouse) {
            el.removeEventListener(
                'mouseenter',
                mouseEnterHandler.bind(null, el)
            );
            el.removeEventListener(
                'mouseleave',
                mouseLeaveHandler.bind(null, el)
            );
        }
    }
}

浅写个demo看看效果

<template>
    <ul v-scroll.mouse class="w-[300px] h-[400px] overflow-auto bg-[darkcyan]">
        <li>1</li>
        <li>2</li>
        ...
    </ul>
</template>

make scroll perfect again!

scrollFastxx.gif

这样我们就实现了一个可以控制滚动速度,支持鼠标移入暂停滚动的通用滚动指令了。

存在问题

第一版就这样上线使用了,但很快哈,啪的一下,我就发现了一些问题:

  • 传入小数时,列表不滚动。
  • 页面缩放后,列表不滚动。

1. 问题原因探究

首先要想解决问题,在不存在魔法的情况下,我们要先寻找问题的原因。

既然小数速度无法滚动,那我们在浏览器上测试一二

让页面向下滚动 0.7, 结果发现还是 0 (⊙o⊙),所以我们下次累加的时候还是 0 + 0.5 无限循环,一直是 0

image.png

随后,我翻阅了一遍 W3C 文档,找到 scrollTo 函数相关部分,不过文档并未直接说top参数的处理会向下去整,反而 interface ScrollOptions 中的 top 正是 double 类型,这说明他实际上是支持小数的哇,那这是为什么?

扫视了好几遍之后,发现了一个频频出现的单词 pixels,这些参数都是以像素为单位的。

像素,像素?像素...啊!道爷我悟了!

可不是嘛,这哪来的 0.5 个像素嘛,这可不得取整?

顺便,在翻阅文档时,也找到了,网页缩放后滚动失效(即使speed >= 1)的原因:W3C文档VisualViewport中找到了这句话,滚动高度会随着页面缩放变小。

image.png

我们在Chrome尝试一下,看看是否属实:

现在正常大小网页设置一下滚动高度,并没有什么问题

image.png

随后,缩放网页到90%,马上哈,Y轴的滚动量就变成 0

image.png

再尝试一下赋值其他的值,会发现,缩放后设置滚动高度后,其真实的滚动量确实减少了,但不是按照我们朴素思维等比例减少的(具体怎么个算法,没找到...)

image.png

不过知道这点就足够了,在当前情况下,想要实现我们要的小数级别的滚动速度,那么我们必然不能直接基于 el.scrollTop 来滚动了,必须有所变通。

2. 问题解决:缓存计算

在哐哐哐一通尝试下(css animation | 改用setTimeout,把间隔时间放长 | etc.),最终我想到了一个破费科特的办法,既能满足我们的需求,又很简单不需要大量改动现有代码:

—— 缓存计算滚动高度

顾名思义,即当el.scrollTop不可靠的时候,那么就由我们自己来手动管理滚动高度,设置一个自定义的变量来对scrollTop进行累加,这样就规避掉了el.scrollTop“只会取整”(并不是),导致设置 0.5 速度后,el.scrollTop一直是0无法累加的问题了。

同时由于 scrollTop 是我们自己进行计算累加的,也不会受到网页缩放的影响了,缩放后也能正常地进行滚动了。

这样即使我们 speed = 0.5 也能够正常“慢速滚动”(本质上非整数的帧滚动高度相同,即达到了速度放慢的效果)

3. 修改后完整代码

PS:需要特别注意的是,将基准滚动高度改为我们的自定义缓存滚动高度后,用户自行滚动的事件是不会自动同步到我们的缓存滚动高度的,所以需要我们自己同步一下。

/**
 * 自动滚动
 *
 * 修饰符:
 * mouse 支持鼠标移入移出暂停动画
 */
import { DirectiveBinding } from 'vue';
interface AnimationElement extends HTMLElement {
    speed: number;
    animationId: number;
    cacheScrollTop: number; // 存放我们缓存的scrollTop
}
const RAF = window.requestAnimationFrame;
const CancelRAF = window.cancelAnimationFrame;
const scrollElement = (el: AnimationElement) => {
    const maxScrollTop = el.scrollHeight - el.clientHeight;
    // 直接在缓存滚动高度上进行计算
    el.cacheScrollTop =
        el.cacheScrollTop + el.speed >= maxScrollTop
            ? 0
            : el.cacheScrollTop + el.speed;
    // 将缓存高度设置为当前滚动高度
    el.scrollTo({
        top: el.cacheScrollTop
    });
    // 执行下一帧
    el.animationId = RAF(scrollElement.bind(null, el));
};
// 鼠标移入暂停
const mouseEnterHandler = (el: AnimationElement) => {
    if (el.animationId) {
        CancelRAF(el.animationId);
        el.animationId = undefined;
    }
};
// 鼠标移出继续运行
const mouseLeaveHandler = (el: AnimationElement) =>
    (el.animationId = RAF(scrollElement.bind(null, el)));
// 处理用户的滚动事件
const elementScrollHandler = (el: AnimationElement) =>
    (el.cacheScrollTop = el.scrollTop);
export default {
    name: 'scroll',
    mounted: (el: AnimationElement, binding: DirectiveBinding) => {
        const maxScrollTop = el.scrollHeight - el.clientHeight;
        // 无需滚动(这里 - 1因为scrollHeight会四舍五入)
        if (maxScrollTop - 1 <= 0) {
            return;
        }
        // 滚动速度
        el.speed = binding.value || 1;
        el.cacheScrollTop = 0;
        el.animationId = RAF(scrollElement.bind(null, el));
        // PS:因为我们使用 cacheScrollTop 来代替 el.scrollTop 处理滚动高度,所以这里需要同步一下用户滚动操作后的 scrollTop ==> 而为了保持动画连贯与流畅,这里千万不要去防抖/节流!
        el.addEventListener('scroll', elementScrollHandler.bind(null, el));
        // 鼠标移入暂停移出继续运动
        if (binding.modifiers.mouse) {
            el.addEventListener('mouseenter', mouseEnterHandler.bind(null, el));
            el.addEventListener('mouseleave', mouseLeaveHandler.bind(null, el));
        }
    },
    unmounted: (el: AnimationElement, binding: DirectiveBinding) => {
        if (binding.modifiers.mouse) {
            el.removeEventListener(
                'mouseenter',
                mouseEnterHandler.bind(null, el)
            );
            el.removeEventListener(
                'mouseleave',
                mouseLeaveHandler.bind(null, el)
            );
        }
    }
};

总结

  • 使用RAF作为滚动动画“框架”
  • 鼠标移入移出动画暂停/恢复,事件监听 + cancelAnimationFrame
  • 滚动的基础单位是像素(1px),正常网页缩放情况下,会向下取整,所以得自行管理滚动高度,对其缓存计算。
  • 网页缩放的情况下,滚动高度会减少,同理也通过缓存计算来解决。

敢敢单单,86 行代码我们就实现了一个基本完美的通用列表滚动指令。

参考资料: W3C CSSOM View Module

到此这篇关于Vue3封装自动滚动列表指令(含网页缩放滚动问题)的文章就介绍到这了,更多相关Vue3 自动滚动列表指令内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 适用于 Vue 的播放器组件Vue-Video-Player操作

    适用于 Vue 的播放器组件Vue-Video-Player操作

    这篇文章主要介绍了适用于 Vue 的播放器组件Vue-Video-Player操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-11-11
  • vue的template模板是如何转为render函数的过程

    vue的template模板是如何转为render函数的过程

    Vue从template到render函数的转换经历模板解析、AST构建、优化、生成渲染函数等步骤,首先进行词法分析将模板拆解为tokens,再进行语法分析构建AST,然后对AST进行静态标记和提升优化,最后转换成JavaScript渲染函数,生成虚拟DOM,完成组件的渲染和更新,实现了模板的高效转化
    2024-10-10
  • vue打包之后的dist文件如何运行

    vue打包之后的dist文件如何运行

    我们知道使用webpack打包vue项目后会生成一个dist文件夹,dist文件夹下有html文件和其他css、js以及图片等,那么打包后的文件该如何正确运行呢?这篇文章主要给大家介绍了关于vue打包之后的dist文件如何运行的相关资料,需要的朋友可以参考下
    2023-05-05
  • vue中进入详情页记住滚动位置的方法(keep-alive)

    vue中进入详情页记住滚动位置的方法(keep-alive)

    今天小编就为大家分享一篇vue中进入详情页记住滚动位置的方法(keep-alive),具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-09-09
  • el-form组件使用resetFields重置失效的问题解决

    el-form组件使用resetFields重置失效的问题解决

    用el-form写了包含三个字段的表单,使用resetFields方法进行重置,发现点击重置或要清空校验时是失效的,所以本文给大家介绍了el-form组件使用resetFields重置失效的问题解决,需要的朋友可以参考下
    2023-12-12
  • ant design的table组件实现全选功能以及自定义分页

    ant design的table组件实现全选功能以及自定义分页

    这篇文章主要介绍了ant design的table组件实现全选功能以及自定义分页,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-11-11
  • vue中LocalStorage与SessionStorage的区别与用法

    vue中LocalStorage与SessionStorage的区别与用法

    本文主要介绍了LocalStorage和SessionStorage。LocalStorage和SessionStorage是两种存储方式,本文详细的介绍一下区别以及用法,感兴趣的可以了解一下
    2021-09-09
  • 基于Vue-Cli 打包自动生成/抽离相关配置文件的实现方法

    基于Vue-Cli 打包自动生成/抽离相关配置文件的实现方法

    基于Vue-cli 项目产品部署,涉及到的交互的地址等配置信息,每次都要重新打包才能生效,极大的降低了效率。这篇文章主要介绍了基于Vue-Cli 打包自动生成/抽离相关配置文件 ,需要的朋友可以参考下
    2018-12-12
  • VUE学习宝典之el-dialog使用示例

    VUE学习宝典之el-dialog使用示例

    在我工作过程中使用el-dialog的需求挺多的,也积累了一下使用技巧,这篇文章主要给大家介绍了关于VUE学习宝典之el-dialog使用的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-03-03
  • Electron+vite+vuetify项目搭建的流程和方法

    Electron+vite+vuetify项目搭建的流程和方法

    最近想用Electron来进行跨平台的桌面应用开发,同时想用vuetify作为组件,于是想搭建一个这样的开发环境,这里分享下Electron+vite+vuetify项目搭建的流程和方法,感兴趣的朋友一起看看吧
    2024-06-06

最新评论