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

使用 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 等原始方法。

核心实现: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-modelfinished 事件。

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 的全部特性,还额外提供了 pauseresumereset 等命令式控制方法,并且完全基于 Vue 3 + TypeScript,零额外依赖。

为什么不用 useTransition

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

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

核心实现: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-modelfinished 事件。

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 的全部特性,还额外提供了 pauseresumereset 等命令式控制方法,并且完全基于 Vue 3 + TypeScript,零额外依赖。

为什么不用 useTransition

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

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

核心实现: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-modelfinished 事件。

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 的全部特性,还额外提供了 pauseresumereset 等命令式控制方法,并且完全基于 Vue 3 + TypeScript,零额外依赖。

为什么不用 useTransition

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

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

核心实现: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-modelfinished 事件。

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 的全部特性,还额外提供了 pauseresumereset 等命令式控制方法,并且完全基于 Vue 3 + TypeScript,零额外依赖。

为什么不用 useTransition

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

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

核心实现: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-modelfinished 事件。

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 的全部特性,还额外提供了 pauseresumereset 等命令式控制方法,并且完全基于 Vue 3 + TypeScript,零额外依赖。

为什么不用 useTransition

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

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

核心实现: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-modelfinished 事件。

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 的强大之处,并在实际项目中灵活运用。

相关推荐
薛定e的猫咪2 小时前
2026 年 4 月实测:OpenAI Codex 保姆级教程,从安装到 MCP、Skills 与多智能体协作
前端·数据库·人工智能
I love studying!!!2 小时前
Web应用程序:用户账户
前端·数据库·sqlite
whuhewei2 小时前
React性能优化
前端·react.js·性能优化
m0_738120722 小时前
渗透知识ctfshow——Web应用安全与防护(三)
android·前端·安全
qq_454245032 小时前
图数据标准化与智能去重框架:设计与实现解析
数据结构·架构·c#·图论
下北沢美食家2 小时前
React面试题2
前端·react.js·前端框架
摇滚侠2 小时前
HTML CSS 演示小米 logo 的变化 border-radius 属性设置圆角
前端·css·html
❆VE❆2 小时前
虚拟列表原理与实战运用场景详解
前端·javascript·css·vue.js·html·虚拟列表
weixin_408099673 小时前
【实战教程】EasyClick 调用 OCR 文字识别 API(自动识别屏幕文字 + 完整示例代码)
前端·人工智能·后端·ocr·api·安卓·easyclick