文章目录
前言
数字滚动效果是前端开发中常见的交互需求,常用于数据可视化、计数器、统计面板等场景。本文将详细介绍如何在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帧率变小造成看起滚动卡顿变慢,实际滚动效果丝滑快速)