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