最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Vue使用VueUse構(gòu)建一個(gè)支持暫停/重置的CountUp組件

 更新時(shí)間:2026年04月13日 08:47:58   作者:禪思院  
本文介紹了如何利用 VueUse 的 useRafFn 從零構(gòu)建一個(gè)功能更強(qiáng)大的 CountUp 組件,它不僅支持 vue-countup-v3 的全部特性,還額外提供了 pause、resume、reset 等命令式控制方法,感興趣的小伙伴可以了解下

使用 VueUse 構(gòu)建一個(gè)支持暫停/重置的 CountUp 組件

告別臃腫的依賴,用組合式 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 的全部特性,還額外提供了 pauseresume、reset 等命令式控制方法,并且完全基于 Vue 3 + TypeScript,零額外依賴。

為什么不用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-modelfinished 事件。

<!-- CountUp.vue -->
<template>
  <span ref="el">{{ formattedValue }}</span>
</template>
<script setup lang="ts">
import { computed, watch } from 'vue'
import { useCountUp } from './hooks/useCountUp'
const props = withDefaults(defineProps<{
  endVal: number
  duration?: number
  decimalPlaces?: number
  autoplay?: boolean
  useGrouping?: boolean
  prefix?: string
  suffix?: string
}>(), {
  duration: 1000,
  decimalPlaces: 0,
  autoplay: true,
  useGrouping: false,
  prefix: '',
  suffix: ''
})
const emit = defineEmits<{
  (e: 'finished'): void
  (e: 'update:modelValue', value: number): void
}>()
const {
  value,
  isAnimating,
  start,
  pause,
  resume,
  reset
} = useCountUp({
  startVal: 0,
  endVal: props.endVal,
  duration: props.duration,
  autoplay: props.autoplay
})
// 格式化顯示
const formattedValue = computed(() => {
  let num = value.value.toFixed(props.decimalPlaces)
  if (props.useGrouping) {
    num = num.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
  }
  return `${props.prefix}${num}${props.suffix}`
})
// 動(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)文章

最新評(píng)論

黎平县| 佳木斯市| 鲁山县| 曲水县| 浮梁县| 岐山县| 璧山县| 嘉荫县| 南漳县| 八宿县| 清水河县| 新巴尔虎右旗| 休宁县| 巫山县| 罗平县| 肃南| 资兴市| 松江区| 郯城县| 同心县| 新建县| 岳西县| 南汇区| 徐汇区| 繁峙县| 平山县| 北安市| 自贡市| 茶陵县| 南部县| 渝北区| 青海省| 新巴尔虎右旗| 通山县| 东方市| 兴海县| 乐昌市| 旌德县| 明星| 维西| 华蓥市|