vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue VueUse构建CountUp组件

Vue使用VueUse构建一个支持暂停/重置的CountUp组件

作者:禅思院

本文介绍了如何利用 VueUse 的 useRafFn 从零构建一个功能更强大的 CountUp 组件,它不仅支持 vue-countup-v3 的全部特性,还额外提供了 pause、resume、reset 等命令式控制方法,感兴趣的小伙伴可以了解下

使用 VueUse 构建一个支持暂停/重置的 CountUp 组件

告别臃肿的依赖,用组合式 API 实现完全可控的数字滚动动画

在日常的前端开发中,数字滚动动画(CountUp)是一个非常常见的需求——从 0 增长到 100 万、实时更新的交易数据、统计看板的关键指标……一个平滑的数字动画能让页面瞬间“活”起来。

社区中已经有不少现成的解决方案,比如 vue-countup-v3。但它有一个明显的局限:只支持自动播放,无法提供暂停、重置等精细控制。如果你的业务需要用户手动启停动画(例如数据对比场景),或者需要根据某些状态重置计数器,这个库就无法满足。

本文介绍如何利用 VueUse 的 useRafFn 从零构建一个功能更强大的 CountUp 组件。它不仅支持 vue-countup-v3 的全部特性,还额外提供了 pauseresumereset 等命令式控制方法,并且完全基于 Vue 3 + TypeScript,零额外依赖。

为什么不用useTransition

VueUse 提供了一个非常优雅的 useTransition,可以轻松实现数值的过渡动画。但它是一个“声明式”工具——你只需要改变源数值,动画会自动完成。这种模式下,你无法在动画中途暂停,也无法跳转到某个中间值后再继续

为了实现命令式控制,我们需要更底层的工具:useRafFn。它基于 requestAnimationFrame 提供了一个可控的动画循环,我们可以精确控制每一帧的计算,并向外暴露 pauseresume 等原始方法。

核心实现:useCountUpHook

我们将动画逻辑封装在一个独立的 Hook 中,方便复用和测试。

// hooks/useCountUp.ts
import { ref, watch } from 'vue'
import { useRafFn } from '@vueuse/core'
interface UseCountUpOptions {
  startVal?: number    // 起始值,默认为 0
  endVal: number       // 结束值
  duration?: number    // 动画时长(毫秒),默认 1000
  autoplay?: boolean   // 是否自动开始,默认 true
}
export function useCountUp(options: UseCountUpOptions) {
  const {
    startVal = 0,
    endVal,
    duration = 1000,
    autoplay = true
  } = options
  const currentValue = ref(startVal)
  const isAnimating = ref(false)
  let startTime = 0
  let startValue = startVal
  let endValue = endVal
  let animDuration = duration
  // 核心动画循环
  const { pause, resume, isActive } = useRafFn(({ timestamp }) => {
    if (!startTime) startTime = timestamp
    const elapsed = timestamp - startTime
    let progress = Math.min(1, elapsed / animDuration)
    // 使用 easeOutCubic 缓动,让动画更自然
    const easeProgress = 1 - Math.pow(1 - progress, 3)
    const newVal = startValue + (endValue - startValue) * easeProgress
    currentValue.value = newVal
    if (progress >= 1) {
      currentValue.value = endValue
      pause()               // 动画结束,停止循环
      isAnimating.value = false
    }
  }, { immediate: false })
  // 开始动画(可指定新的结束值和时长)
  const start = (newEndVal?: number, newDuration?: number) => {
    if (newEndVal !== undefined) endValue = newEndVal
    if (newDuration !== undefined) animDuration = newDuration
    startValue = currentValue.value   // 从当前值开始
    startTime = 0
    isAnimating.value = true
    resume()
  }
  // 重置到起始值并停止动画
  const reset = () => {
    pause()
    isAnimating.value = false
    currentValue.value = startVal
    startTime = 0
  }
  // 自动开始
  if (autoplay) {
    start()
  }
  // 监听外部 endVal 变化,自动触发新动画
  watch(() => endVal, (newVal) => {
    if (!isAnimating.value) {
      start(newVal)
    } else {
      // 如果动画进行中,只更新目标值,不中断当前动画
      endValue = newVal
    }
  })
  return {
    value: currentValue,     // 当前动画值(响应式)
    isAnimating,             // 是否正在动画中
    start,                   // 开始/重新开始
    pause,                   // 暂停
    resume,                  // 恢复
    reset,                   // 重置
    isActive                 // useRafFn 内部状态
  }
}

关键点解析:

封装CountUp组件

有了 Hook,组件层的代码就非常简洁了。我们还需要支持格式化、前缀后缀、v-modelfinished 事件。

<!-- CountUp.vue -->
<template>
  <span ref="el">{{ formattedValue }}</span>
</template>
<script setup lang="ts">
import { computed, watch } from 'vue'
import { useCountUp } from './hooks/useCountUp'
const props = withDefaults(defineProps<{
  endVal: number
  duration?: number
  decimalPlaces?: number
  autoplay?: boolean
  useGrouping?: boolean
  prefix?: string
  suffix?: string
}>(), {
  duration: 1000,
  decimalPlaces: 0,
  autoplay: true,
  useGrouping: false,
  prefix: '',
  suffix: ''
})
const emit = defineEmits<{
  (e: 'finished'): void
  (e: 'update:modelValue', value: number): void
}>()
const {
  value,
  isAnimating,
  start,
  pause,
  resume,
  reset
} = useCountUp({
  startVal: 0,
  endVal: props.endVal,
  duration: props.duration,
  autoplay: props.autoplay
})
// 格式化显示
const formattedValue = computed(() => {
  let num = value.value.toFixed(props.decimalPlaces)
  if (props.useGrouping) {
    num = num.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
  }
  return `${props.prefix}${num}${props.suffix}`
})
// 动画结束时触发
watch(isAnimating, (val) => {
  if (!val) emit('finished')
})
// 支持 v-model
watch(value, (newVal) => {
  emit('update:modelValue', newVal)
})
// 暴露控制方法给父组件
defineExpose({
  start,
  pause,
  resume,
  reset,
  isAnimating
})
</script>

使用示例:暂停 / 重置 / 动态跳转

父组件通过 ref 获取组件实例,即可随心所欲地控制动画。

<template>
  <div>
    <CountUp
      ref="countUpRef"
      :endVal="target"
      :duration="2000"
      :decimalPlaces="0"
      @finished="onFinished"
    />
    <div style="margin-top: 16px; display: flex; gap: 8px;">
      <button @click="countUpRef?.start(9999, 1000)">跳到 9999 (1秒)</button>
      <button @click="countUpRef?.pause()">暂停</button>
      <button @click="countUpRef?.resume()">恢复</button>
      <button @click="countUpRef?.reset()">重置</button>
      <button @click="target = 5000">改变目标值为 5000</button>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import CountUp from './components/CountUp.vue'
const countUpRef = ref<InstanceType<typeof CountUp>>()
const target = ref(2025)
const onFinished = () => {
  console.log('动画完成')
}
</script>

与vue-countup-v3功能对比

功能vue-countup-v3本文组件说明
startVal / endVal
duration(秒)✅(毫秒)改为毫秒更符合常规
autoplay
prefix / suffix
decimalPlaces
useGrouping
easing 缓动可扩展默认 easeOutCubic,可轻松替换
loop 循环未内置可通过 @finished 重新调用 start 实现
pause / resume独有
reset独有
@finished 事件
v-model双向绑定当前值
TypeScript有限支持✅ 完美

总结与扩展

通过 useRafFn 我们获得了动画循环的完全控制权,从而实现了比现有社区库更灵活的交互能力。整个实现仅有 100 行左右的核心代码,却具备了企业级组件应有的所有特性。

如果你需要进一步扩展,还可以轻松添加:

VueUse 生态提供了大量可组合工具,useRafFn 只是其中之一。当你需要任何形式的可控动画、绘图、轮询时,它都能派上用场。希望这篇文章能帮助你更好地理解 Vue 3 组合式 API 的强大之处,并在实际项目中灵活运用。

到此这篇关于Vue使用VueUse构建一个支持暂停/重置的CountUp组件的文章就介绍到这了,更多相关Vue VueUse构建CountUp组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文