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

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

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

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

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

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

为什么不用 useTransition?

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

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

核心实现:useCountUp Hook

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

typescript 复制代码
// 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 内部状态
  }
}

关键点解析:

  • useRafFn 会在每一帧执行回调,我们根据已用时间与总时长的比例计算当前值。
  • 使用 easeOutCubic 缓动,让动画"快慢快"更自然。
  • start 方法允许从当前值过渡到新目标值,非常适合动态更新场景。
  • 外部改变 endVal 时,如果动画空闲则直接开始新动画,否则只更新目标值,保证流畅性。

封装 CountUp 组件

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

vue 复制代码
<!-- 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 获取组件实例,即可随心所欲地控制动画。

vue 复制代码
<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 行左右的核心代码,却具备了企业级组件应有的所有特性。

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

  • 自定义缓动函数 :在 Hook 中替换 easeProgress 的计算公式即可。
  • 支持循环播放 :在动画结束时判断是否需要重新调用 start。
  • 起始值动态变化 :将 startVal 也作为响应式 prop,并在 reset 中同步更新。
  • 更精确的节流 :useRafFn 本身已做了帧级节流,无需额外处理。

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

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

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

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

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

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

为什么不用 useTransition?

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

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

核心实现:useCountUp Hook

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

typescript 复制代码
// 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 内部状态
  }
}

关键点解析:

  • useRafFn 会在每一帧执行回调,我们根据已用时间与总时长的比例计算当前值。
  • 使用 easeOutCubic 缓动,让动画"快慢快"更自然。
  • start 方法允许从当前值过渡到新目标值,非常适合动态更新场景。
  • 外部改变 endVal 时,如果动画空闲则直接开始新动画,否则只更新目标值,保证流畅性。

封装 CountUp 组件

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

vue 复制代码
<!-- 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 获取组件实例,即可随心所欲地控制动画。

vue 复制代码
<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 行左右的核心代码,却具备了企业级组件应有的所有特性。

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

  • 自定义缓动函数 :在 Hook 中替换 easeProgress 的计算公式即可。
  • 支持循环播放 :在动画结束时判断是否需要重新调用 start。
  • 起始值动态变化 :将 startVal 也作为响应式 prop,并在 reset 中同步更新。
  • 更精确的节流 :useRafFn 本身已做了帧级节流,无需额外处理。

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

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

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

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

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

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

为什么不用 useTransition?

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

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

核心实现:useCountUp Hook

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

typescript 复制代码
// 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 内部状态
  }
}

关键点解析:

  • useRafFn 会在每一帧执行回调,我们根据已用时间与总时长的比例计算当前值。
  • 使用 easeOutCubic 缓动,让动画"快慢快"更自然。
  • start 方法允许从当前值过渡到新目标值,非常适合动态更新场景。
  • 外部改变 endVal 时,如果动画空闲则直接开始新动画,否则只更新目标值,保证流畅性。

封装 CountUp 组件

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

vue 复制代码
<!-- 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 获取组件实例,即可随心所欲地控制动画。

vue 复制代码
<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 行左右的核心代码,却具备了企业级组件应有的所有特性。

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

  • 自定义缓动函数 :在 Hook 中替换 easeProgress 的计算公式即可。
  • 支持循环播放 :在动画结束时判断是否需要重新调用 start。
  • 起始值动态变化 :将 startVal 也作为响应式 prop,并在 reset 中同步更新。
  • 更精确的节流 :useRafFn 本身已做了帧级节流,无需额外处理。

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

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

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

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

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

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

为什么不用 useTransition?

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

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

核心实现:useCountUp Hook

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

typescript 复制代码
// 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 内部状态
  }
}

关键点解析:

  • useRafFn 会在每一帧执行回调,我们根据已用时间与总时长的比例计算当前值。
  • 使用 easeOutCubic 缓动,让动画"快慢快"更自然。
  • start 方法允许从当前值过渡到新目标值,非常适合动态更新场景。
  • 外部改变 endVal 时,如果动画空闲则直接开始新动画,否则只更新目标值,保证流畅性。

封装 CountUp 组件

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

vue 复制代码
<!-- 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 获取组件实例,即可随心所欲地控制动画。

vue 复制代码
<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 行左右的核心代码,却具备了企业级组件应有的所有特性。

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

  • 自定义缓动函数 :在 Hook 中替换 easeProgress 的计算公式即可。
  • 支持循环播放 :在动画结束时判断是否需要重新调用 start。
  • 起始值动态变化 :将 startVal 也作为响应式 prop,并在 reset 中同步更新。
  • 更精确的节流 :useRafFn 本身已做了帧级节流,无需额外处理。

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

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

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

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

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

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

为什么不用 useTransition?

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

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

核心实现:useCountUp Hook

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

typescript 复制代码
// 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 内部状态
  }
}

关键点解析:

  • useRafFn 会在每一帧执行回调,我们根据已用时间与总时长的比例计算当前值。
  • 使用 easeOutCubic 缓动,让动画"快慢快"更自然。
  • start 方法允许从当前值过渡到新目标值,非常适合动态更新场景。
  • 外部改变 endVal 时,如果动画空闲则直接开始新动画,否则只更新目标值,保证流畅性。

封装 CountUp 组件

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

vue 复制代码
<!-- 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 获取组件实例,即可随心所欲地控制动画。

vue 复制代码
<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 行左右的核心代码,却具备了企业级组件应有的所有特性。

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

  • 自定义缓动函数 :在 Hook 中替换 easeProgress 的计算公式即可。
  • 支持循环播放 :在动画结束时判断是否需要重新调用 start。
  • 起始值动态变化 :将 startVal 也作为响应式 prop,并在 reset 中同步更新。
  • 更精确的节流 :useRafFn 本身已做了帧级节流,无需额外处理。

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

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

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

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

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

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

为什么不用 useTransition?

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

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

核心实现:useCountUp Hook

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

typescript 复制代码
// 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 内部状态
  }
}

关键点解析:

  • useRafFn 会在每一帧执行回调,我们根据已用时间与总时长的比例计算当前值。
  • 使用 easeOutCubic 缓动,让动画"快慢快"更自然。
  • start 方法允许从当前值过渡到新目标值,非常适合动态更新场景。
  • 外部改变 endVal 时,如果动画空闲则直接开始新动画,否则只更新目标值,保证流畅性。

封装 CountUp 组件

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

vue 复制代码
<!-- 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 获取组件实例,即可随心所欲地控制动画。

vue 复制代码
<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 行左右的核心代码,却具备了企业级组件应有的所有特性。

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

  • 自定义缓动函数 :在 Hook 中替换 easeProgress 的计算公式即可。
  • 支持循环播放 :在动画结束时判断是否需要重新调用 start。
  • 起始值动态变化 :将 startVal 也作为响应式 prop,并在 reset 中同步更新。
  • 更精确的节流 :useRafFn 本身已做了帧级节流,无需额外处理。

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

相关推荐
天空属于哈夫克33 天前
企业微信二次开发:精准实现关键词自动回复
架构·企业微信
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万3 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝3 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
A黄俊辉A3 天前
uniapp webview中实现 app和内嵌的H5双向通信
vue.js·json
三十而立洋3 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
晨米酱3 天前
AGENTS.md:Agent 的上下文策略层
面试·架构·agent
这个DBA有点耶3 天前
MVCC深入:Read View、版本链与快照读——InnoDB并发控制的内核
数据库·mysql·架构
卡布鲁3 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js