Vue使用VueUse構(gòu)建一個(gè)支持暫停/重置的CountUp組件
使用 VueUse 構(gòu)建一個(gè)支持暫停/重置的 CountUp 組件
告別臃腫的依賴(lài),用組合式 API 實(shí)現(xiàn)完全可控的數(shù)字滾動(dòng)動(dòng)畫(huà)
在日常的前端開(kāi)發(fā)中,數(shù)字滾動(dòng)動(dòng)畫(huà)(CountUp)是一個(gè)非常常見(jiàn)的需求——從 0 增長(zhǎng)到 100 萬(wàn)、實(shí)時(shí)更新的交易數(shù)據(jù)、統(tǒng)計(jì)看板的關(guān)鍵指標(biāo)……一個(gè)平滑的數(shù)字動(dòng)畫(huà)能讓頁(yè)面瞬間“活”起來(lái)。
社區(qū)中已經(jīng)有不少現(xiàn)成的解決方案,比如 vue-countup-v3。但它有一個(gè)明顯的局限:只支持自動(dòng)播放,無(wú)法提供暫停、重置等精細(xì)控制。如果你的業(yè)務(wù)需要用戶手動(dòng)啟停動(dòng)畫(huà)(例如數(shù)據(jù)對(duì)比場(chǎng)景),或者需要根據(jù)某些狀態(tài)重置計(jì)數(shù)器,這個(gè)庫(kù)就無(wú)法滿足。
本文介紹如何利用 VueUse 的 useRafFn 從零構(gòu)建一個(gè)功能更強(qiáng)大的 CountUp 組件。它不僅支持 vue-countup-v3 的全部特性,還額外提供了 pause、resume、reset 等命令式控制方法,并且完全基于 Vue 3 + TypeScript,零額外依賴(lài)。
為什么不用useTransition
VueUse 提供了一個(gè)非常優(yōu)雅的 useTransition,可以輕松實(shí)現(xiàn)數(shù)值的過(guò)渡動(dòng)畫(huà)。但它是一個(gè)“聲明式”工具——你只需要改變?cè)磾?shù)值,動(dòng)畫(huà)會(huì)自動(dòng)完成。這種模式下,你無(wú)法在動(dòng)畫(huà)中途暫停,也無(wú)法跳轉(zhuǎn)到某個(gè)中間值后再繼續(xù)。
為了實(shí)現(xiàn)命令式控制,我們需要更底層的工具:useRafFn。它基于 requestAnimationFrame 提供了一個(gè)可控的動(dòng)畫(huà)循環(huán),我們可以精確控制每一幀的計(jì)算,并向外暴露 pause、resume 等原始方法。
核心實(shí)現(xiàn):useCountUpHook
我們將動(dòng)畫(huà)邏輯封裝在一個(gè)獨(dú)立的 Hook 中,方便復(fù)用和測(cè)試。
// hooks/useCountUp.ts
import { ref, watch } from 'vue'
import { useRafFn } from '@vueuse/core'
interface UseCountUpOptions {
startVal?: number // 起始值,默認(rèn)為 0
endVal: number // 結(jié)束值
duration?: number // 動(dòng)畫(huà)時(shí)長(zhǎng)(毫秒),默認(rèn) 1000
autoplay?: boolean // 是否自動(dòng)開(kāi)始,默認(rèn) 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
// 核心動(dòng)畫(huà)循環(huán)
const { pause, resume, isActive } = useRafFn(({ timestamp }) => {
if (!startTime) startTime = timestamp
const elapsed = timestamp - startTime
let progress = Math.min(1, elapsed / animDuration)
// 使用 easeOutCubic 緩動(dòng),讓動(dòng)畫(huà)更自然
const easeProgress = 1 - Math.pow(1 - progress, 3)
const newVal = startValue + (endValue - startValue) * easeProgress
currentValue.value = newVal
if (progress >= 1) {
currentValue.value = endValue
pause() // 動(dòng)畫(huà)結(jié)束,停止循環(huán)
isAnimating.value = false
}
}, { immediate: false })
// 開(kāi)始動(dòng)畫(huà)(可指定新的結(jié)束值和時(shí)長(zhǎng))
const start = (newEndVal?: number, newDuration?: number) => {
if (newEndVal !== undefined) endValue = newEndVal
if (newDuration !== undefined) animDuration = newDuration
startValue = currentValue.value // 從當(dāng)前值開(kāi)始
startTime = 0
isAnimating.value = true
resume()
}
// 重置到起始值并停止動(dòng)畫(huà)
const reset = () => {
pause()
isAnimating.value = false
currentValue.value = startVal
startTime = 0
}
// 自動(dòng)開(kāi)始
if (autoplay) {
start()
}
// 監(jiān)聽(tīng)外部 endVal 變化,自動(dòng)觸發(fā)新動(dòng)畫(huà)
watch(() => endVal, (newVal) => {
if (!isAnimating.value) {
start(newVal)
} else {
// 如果動(dòng)畫(huà)進(jìn)行中,只更新目標(biāo)值,不中斷當(dāng)前動(dòng)畫(huà)
endValue = newVal
}
})
return {
value: currentValue, // 當(dāng)前動(dòng)畫(huà)值(響應(yīng)式)
isAnimating, // 是否正在動(dòng)畫(huà)中
start, // 開(kāi)始/重新開(kāi)始
pause, // 暫停
resume, // 恢復(fù)
reset, // 重置
isActive // useRafFn 內(nèi)部狀態(tài)
}
}關(guān)鍵點(diǎn)解析:
useRafFn會(huì)在每一幀執(zhí)行回調(diào),我們根據(jù)已用時(shí)間與總時(shí)長(zhǎng)的比例計(jì)算當(dāng)前值。- 使用
easeOutCubic緩動(dòng),讓動(dòng)畫(huà)“快慢快”更自然。 start方法允許從當(dāng)前值過(guò)渡到新目標(biāo)值,非常適合動(dòng)態(tài)更新場(chǎng)景。- 外部改變
endVal時(shí),如果動(dòng)畫(huà)空閑則直接開(kāi)始新動(dòng)畫(huà),否則只更新目標(biāo)值,保證流暢性。
封裝CountUp組件
有了 Hook,組件層的代碼就非常簡(jiǎn)潔了。我們還需要支持格式化、前綴后綴、v-model 和 finished 事件。
<!-- 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}`
})
// 動(dòng)畫(huà)結(jié)束時(shí)觸發(fā)
watch(isAnimating, (val) => {
if (!val) emit('finished')
})
// 支持 v-model
watch(value, (newVal) => {
emit('update:modelValue', newVal)
})
// 暴露控制方法給父組件
defineExpose({
start,
pause,
resume,
reset,
isAnimating
})
</script>使用示例:暫停 / 重置 / 動(dòng)態(tài)跳轉(zhuǎn)
父組件通過(guò) ref 獲取組件實(shí)例,即可隨心所欲地控制動(dòng)畫(huà)。
<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()">恢復(fù)</button>
<button @click="countUpRef?.reset()">重置</button>
<button @click="target = 5000">改變目標(biāo)值為 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('動(dòng)畫(huà)完成')
}
</script>與vue-countup-v3功能對(duì)比
| 功能 | vue-countup-v3 | 本文組件 | 說(shuō)明 |
|---|---|---|---|
startVal / endVal | ? | ? | |
duration(秒) | ? | ?(毫秒) | 改為毫秒更符合常規(guī) |
autoplay | ? | ? | |
prefix / suffix | ? | ? | |
decimalPlaces | ? | ? | |
useGrouping | ? | ? | |
easing 緩動(dòng) | ? | 可擴(kuò)展 | 默認(rèn) easeOutCubic,可輕松替換 |
loop 循環(huán) | ? | 未內(nèi)置 | 可通過(guò) @finished 重新調(diào)用 start 實(shí)現(xiàn) |
pause / resume | ? | ? | 獨(dú)有 |
reset | ? | ? | 獨(dú)有 |
@finished 事件 | ? | ? | |
v-model | ? | ? | 雙向綁定當(dāng)前值 |
| TypeScript | 有限支持 | ? 完美 |
總結(jié)與擴(kuò)展
通過(guò) useRafFn 我們獲得了動(dòng)畫(huà)循環(huán)的完全控制權(quán),從而實(shí)現(xiàn)了比現(xiàn)有社區(qū)庫(kù)更靈活的交互能力。整個(gè)實(shí)現(xiàn)僅有 100 行左右的核心代碼,卻具備了企業(yè)級(jí)組件應(yīng)有的所有特性。
如果你需要進(jìn)一步擴(kuò)展,還可以輕松添加:
- 自定義緩動(dòng)函數(shù):在 Hook 中替換
easeProgress的計(jì)算公式即可。 - 支持循環(huán)播放:在動(dòng)畫(huà)結(jié)束時(shí)判斷是否需要重新調(diào)用
start。 - 起始值動(dòng)態(tài)變化:將
startVal也作為響應(yīng)式 prop,并在reset中同步更新。 - 更精確的節(jié)流:
useRafFn本身已做了幀級(jí)節(jié)流,無(wú)需額外處理。
VueUse 生態(tài)提供了大量可組合工具,useRafFn 只是其中之一。當(dāng)你需要任何形式的可控動(dòng)畫(huà)、繪圖、輪詢時(shí),它都能派上用場(chǎng)。希望這篇文章能幫助你更好地理解 Vue 3 組合式 API 的強(qiáng)大之處,并在實(shí)際項(xiàng)目中靈活運(yùn)用。
到此這篇關(guān)于Vue使用VueUse構(gòu)建一個(gè)支持暫停/重置的CountUp組件的文章就介紹到這了,更多相關(guān)Vue VueUse構(gòu)建CountUp組件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue3+vantUI3時(shí)間組件封裝過(guò)程支持選擇年以及年月日時(shí)分秒
這篇文章主要介紹了Vue3+vantUI3時(shí)間組件封裝過(guò)程支持選擇年以及年月日時(shí)分秒,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-07-07
element使用自定義icon圖標(biāo)的詳細(xì)步驟
前端經(jīng)常會(huì)用到UI提供的各種圖表,推薦阿里的圖標(biāo)庫(kù),下面這篇文章主要給大家介紹了關(guān)于element使用自定義icon圖標(biāo)的詳細(xì)步驟,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-11-11
示例vue 的keep-alive緩存功能的實(shí)現(xiàn)
這篇文章主要介紹了示例vue 的keep-alive緩存功能的實(shí)現(xiàn),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-12-12
vue+element-ui+sortable.js實(shí)現(xiàn)表格拖拽功能
這篇文章主要為大家詳細(xì)介紹了vue+element-ui+sortable.js實(shí)現(xiàn)表格拖拽功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04
在vue中高德地圖引入和軌跡的繪制的實(shí)現(xiàn)
這篇文章主要介紹了在vue中高德地圖引入和軌跡的繪制的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10
vue el-switch綁定數(shù)值時(shí)需要注意的問(wèn)題
在Vue中使用`el-switch`組件時(shí),綁定數(shù)值類(lèi)型時(shí)應(yīng)使用布爾值(true/false),而綁定字符串類(lèi)型時(shí)應(yīng)使用字符串('true'/'false')2024-12-12
Vue3+TS+Vant3+Pinia(H5端)配置教程詳解
這篇文章主要介紹了Vue3+TS+Vant3+Pinia(H5端)配置教程詳解,需要的朋友可以參考下2023-01-01
Vue-router結(jié)合transition實(shí)現(xiàn)app前進(jìn)后退動(dòng)畫(huà)切換效果的實(shí)例
下面小編就為大家?guī)?lái)一篇Vue-router結(jié)合transition實(shí)現(xiàn)app前進(jìn)后退動(dòng)畫(huà)切換效果的實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10
Vue項(xiàng)目啟動(dòng)后如何在瀏覽器自動(dòng)打開(kāi)
這篇文章主要介紹了Vue項(xiàng)目啟動(dòng)后如何在瀏覽器自動(dòng)打開(kāi)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08

