Vue3通过自定义指令实现数字滚动动画效果


前言

        数字滚动效果是前端开发中常见的交互需求,常用于数据可视化、计数器、统计面板等场景。本文将详细介绍如何在vue3中 实现流畅的数字滚动动画效果


一、什么是数字滚动效果?

        数字滚动效果是指让数字从一个值平滑过渡到另一个值的动画效果,通常表现为数字逐位递增或递减,模拟机械计数器的滚动效果。这种效果可以让数据展示更加生动,提升用户体验。

请添加图片描述

二、实现方式

在 Vue 中实现数字滚动效果主要有几种方式:

1、通过自定义组件实现
2、通过自定义指令实现

本文将重点介绍基于vue3自定义指令实现方案,这是最优雅、使用最简单的方式


三、vue3自定义指令使用回顾

在 Vue3 的生态体系中,自定义指令(Directive)是一个强大的工具,允许开发者直接操作 DOM 元素,实现组件难以覆盖的底层逻辑。无论是表单焦点管理、数据可视化还是性能优化(如图片懒加载),自定义指令都能提供优雅的解决方案。

3.1 生命周期钩子

钩子函数调用时机常用场景
created绑定元素的 attribute 或事件监听器应用之前初始化操作,事件监听器设置
beforeMount指令第一次绑定到元素时初始DOM操作前准备工作
mounted绑定元素插入父DOM后主要的DOM操作
beforeUpdate组件更新前更新前清理工作
updated组件及子组件更新后更新后DOM操作
beforeUnmount卸载绑定元素前清理定时器、事件监听等
unmounted指令与元素解绑后最终清理操作

需要注意的是beforeUpdate、updated触发时机是指令绑定的dom所在的组件更新时触发,而非指令绑定值改变时触发,即使不改变值也可能触发。

示例:

app.directive('focus', {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode) {
    // 下面会介绍各个参数的细节
  },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode) {}
}
});

周期函数入参说明

  • el:指令绑定的 DOM 元素,能用于直接操作 DOM

  • binding:是一个对象,包含以下属性:
    1、 value:指令的绑定值,比如在v-my-directive="1 + 1"中,value 的值就是 2。
    2、 oldValue:指令绑定的前一个值,仅在update和beforeUpdate钩子中可用,无论值是否发生变化都会被传递。
    3、 arg: 传递给指令的参数,例如在v-my-directive:foo中,arg 的值就是 “foo”。
    4、 modifiers:一个包含修饰符的对象,例如在v-my-directive.foo.bar中,modifiers 对象就是{ foo: true, bar: true }。

  • vnode:当前虚拟节点

  • prevNode:上一个虚拟节点,仅在update和beforeUpdate钩子中可用。

3.2 全局与局部指令注册

全局指令(在 main.js 中注册)

import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

// 注册全局指令v-focus
app.directive('focus', {
  mounted(el) {
    el.focus(); // 元素挂载后自动聚焦
  }
});

app.mount('#app');

局部指令(在组件中注册)

export default {
  directives: {
    focus: {
      mounted(el) {
        el.focus();
      }
    }
  }
};

3.3 参数、修饰符与动态绑定

1、 指令参数(Arg)

<div v-highlight:red>红色高亮</div>
<div v-highlight:blue>蓝色高亮</div>
app.directive('highlight', {
  mounted(el, binding) {
    // binding.arg获取参数值('red'或'blue')
    el.style.backgroundColor = binding.arg;
  }
});

2 修饰符(Modifiers)

通过点语法添加修饰符,改变指令默认行为:

<div v-hover-color.red.bold>带修饰符的悬停效果</div>
app.directive('hover-color', {
  mounted(el, binding) {
    // binding.modifiers获取修饰符对象 { red: true, bold: true }
    if (binding.modifiers.red) {
      el.style.color = 'red';
    }
    if (binding.modifiers.bold) {
      el.style.fontWeight = 'bold';
    }
  }
});

3 动态参数

使用中括号实现参数动态化:

<div v-bind:[(attributeName)]="value">动态绑定属性</div>
const attributeName=ref('title')

四、数字滚屏功能设计和代码实现解析

1、功能设计

在规定时间内按固定频率变化数字,使得从0到目标值等差递增并在页面显示当前值,此过程就形成数字滚动连续动画。

例如 :给定目标数值为10,动画时长为1000毫秒,从0开始显示,每隔100毫秒数值增加1,就能看到页面数字从0,1,2,3…10变化过程。

当递增值(等差值)不够1时,限制最低为1,并调整间隔时间,且每次显示数值取整数

2、代码实现解析

希望调用方式

方式1:

    <div v-numScrolling="20"></div>

直接传递目标数值,此方式动画时长采用默认值

方式2:

    <div v-numScrolling="{value:20,duration:2000}">0</div>

传递对象,其中value表示目标数值,duration表示动画时长

我们封装自定义指令希望同时支持2种传参形式


自定义指令内部实现解析

假设目标数值为targetValue,动画时长为duration, 每次数值变化间隔时间为interval,可计算出:

  • 总变化次数:
//变化次数
const count= Math.ceil(duration / interval);
  • 每次递增值(等差值):
//每次递增值
let n = targetValue / count; 

当n算出来小于1时(例如targetValue =10,duration=1000ms,interval=50ms),取整显示可能出现连续2次或多次数值一样情况造成动画不连贯,所以规定n大于等于1,此时重新计算间隔时间

if (n < 1) {
      //小于1默认每次累加1,重新计算间隔时间
      interval = duration / targetValue;
      n = 1;
    }

后续就可通过定时器递增数值显示:

let currentValue=0;//当前累加值(可能带小数)
let intervalId= setInterval(()=>{
  if(currentValue < targetValue){
    //通过Math.min限制超出最大值
    currentValue= Math.min(currentValue + n, targetValue);
     el.textContent = Math.round(currentValue);//取整显示到页面
  }
  else{
    //结束滚动
    clearInterval(intervalId)
    intervalId=null
  }
},interval )

五、完整代码

src/directive/numScrolling.js(自定义指令文件)

export function useNumScrollingDirective(app) {
  app.directive("numScrolling", {
    //挂载
    mounted(el, binding, vnode) {
      handleScrolling(binding, el, vnode);
    },
    //更新
    updated(el, binding, vnode) {
      let { targetValue, oldValue } = getValues(binding);
      //值有变才执行数字滚动
      if (targetValue !== oldValue) {
        handleScrolling(binding, el, vnode);
      }
    },
  });

  //获取目标数值、旧数值(上一次)\动画时长
  function getValues(binding) {
    let targetValue = 0; //最终显示的数字
    let oldValue = 0; //旧数字
    let param = binding.value; //入参
    let duration = 1500; //默认1500毫秒

    /*
     *入参为对象类型
     *例如:{value:18,duration:5000}
     */
    if (typeof param == "object") {
      targetValue = param.value;
      oldValue = binding?.oldValue?.value;
      duration = param.duration || 1500;
    }
    //入参为数字类型
    else if (typeof param == "number") {
      targetValue = param;
      oldValue = binding.oldValue;
    }
    //入参为字符串
    else if (typeof param == "string") {
      targetValue = Number(param);
      oldValue = Number(binding.oldValue);
    }

    return {
      targetValue,
      oldValue,
      duration,
    };
  }

  //数字滚动处理
  function handleScrolling(binding, el, vnode) {
    //数据挂载到当前节点对象,防止全局混用污染
    vnode.currentValue = 0; //当前显示的值
    vnode.intervalId = null; //定时器id
    let { targetValue, duration } = getValues(binding); //targetValue:最终显示的数字
    vnode.duration = duration; //滚动时长,默认1500毫秒
    //0不滚动
    if (targetValue == 0) {
      el.textContent = 0;
      return;
    }

    if (vnode.intervalId !== null) {
      reset(); //重置数据
    }

    let interval = 50; //每次滚动(数字变化)间隔时间(毫秒)
    let count = Math.ceil(vnode.duration / interval); //变化次数
    let n = targetValue / count; //每次累加数值
    if (n < 1) {
      //小于1默认每次累加1,重新计算间隔时间
      interval = vnode.duration / targetValue;
      n = 1;
    }

    //定时改变数值
    vnode.intervalId = setInterval(() => {
      //未累加到目标值
      if (vnode.currentValue < targetValue) {
        vnode.currentValue = Math.min(vnode.currentValue + n, targetValue);
        el.textContent = Math.round(vnode.currentValue); //显示整数
      }
      //停止变化、重置数据
      else {
        reset(vnode);
      }
    }, interval);
  }

  /**
   *
   * 清除定时器,重置数据
   */
  function reset(vnode) {
    vnode.intervalId && clearInterval(vnode.intervalId);
    vnode.intervalId = null;
    vnode.currentValue = 0;
  }
}

说明:上述代码默认动画时长1500ms(支持自定义传参),默认变化间隔时间50ms,可以根据实际效果调整参数。需要注意的是updated周期函数内需要比较旧值和当前值是否变化才执行滚动动画。注意到我们把数据信息(定时器id,当前值、动画时长)存储到vnode(当前虚拟节点)对象上而非顶层全局变量,防止变量混用污染。

全局注册:
main.js

import {useNumScrollingDirective} from './directive/numScrolling.js'

const app = createApp(App)
useNumScrollingDirective(app)
app.mount('#app')

页面调用:

<template>
  <div>
    <div v-numScrolling="11254">0</div>
    <div v-numScrolling="2">0</div>
    <div v-numScrolling="{ value: 18, duration: 3000 }">0</div>
    <div v-numScrolling="{ value: 262, duration: 3000 }">0</div>
    <div v-numScrolling="num">0</div>
  </div>
</template>
<script setup>
import { ref } from "vue";
const num = ref(285);

//4000ms后改变数值
setTimeout(() => {
  num.value = 17;
}, 4000);
</script>

运行结果:

请添加图片描述
(ps:由于视频转gif帧率变小造成看起滚动卡顿变慢,实际滚动效果丝滑快速)


总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

pixle0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值