vue3數(shù)據(jù)可視化實(shí)現(xiàn)數(shù)字滾動(dòng)特效代碼
前言
vue3不支持vue-count-to插件,無法使用vue-count-to實(shí)現(xiàn)數(shù)字動(dòng)效,數(shù)字自動(dòng)分割,vue-count-to主要針對(duì)vue2使用,vue3按照會(huì)報(bào)錯(cuò):TypeError: Cannot read properties of undefined (reading '_c')
的錯(cuò)誤信息。這個(gè)時(shí)候我們只能自己封裝一個(gè)CountTo組件實(shí)現(xiàn)數(shù)字動(dòng)效。先來看效果圖:

思路
使用Vue.component定義公共組件,使用window.requestAnimationFrame(首選,次選setTimeout)來循環(huán)數(shù)字動(dòng)畫,window.cancelAnimationFrame取消數(shù)字動(dòng)畫效果,封裝一個(gè)requestAnimationFrame.js公共文件,CountTo.vue組件,入口導(dǎo)出文件index.js。
文件目錄

使用示例
<CountTo :start="0" // 從數(shù)字多少開始 :end="endCount" // 到數(shù)字多少結(jié)束 :autoPlay="true" // 自動(dòng)播放 :duration="3000" // 過渡時(shí)間 prefix="¥" // 前綴符號(hào) suffix="rmb" // 后綴符號(hào) />
入口文件index.js
const UILib = {
install(Vue) {
Vue.component('CountTo', CountTo)
}
}
export default UILibmain.js使用
import CountTo from './components/count-to/index'; app.use(CountTo)
requestAnimationFrame.js思路
- 先判斷是不是瀏覽器還是其他環(huán)境
- 如果是瀏覽器判斷瀏覽器內(nèi)核類型
- 如果瀏覽器不支持requestAnimationFrame,cancelAnimationFrame方法,改寫setTimeout定時(shí)器
- 導(dǎo)出兩個(gè)方法 requestAnimationFrame, cancelAnimationFrame
各個(gè)瀏覽器前綴:let prefixes = 'webkit moz ms o';
判斷是不是瀏覽器:let isServe = typeof window == 'undefined';
增加各個(gè)瀏覽器前綴:
let prefix;
let requestAnimationFrame;
let cancelAnimationFrame;
// 通過遍歷各瀏覽器前綴,來得到requestAnimationFrame和cancelAnimationFrame在當(dāng)前瀏覽器的實(shí)現(xiàn)形式
for (let i = 0; i < prefixes.length; i++) {
if (requestAnimationFrame && cancelAnimationFrame) { break }
prefix = prefixes[i]
requestAnimationFrame = requestAnimationFrame || window[prefix + 'RequestAnimationFrame']
cancelAnimationFrame = cancelAnimationFrame || window[prefix + 'CancelAnimationFrame'] || window[prefix + 'CancelRequestAnimationFrame']
}
//不支持使用setTimeout方式替換:模擬60幀的效果
// 如果當(dāng)前瀏覽器不支持requestAnimationFrame和cancelAnimationFrame,則會(huì)退到setTimeout
if (!requestAnimationFrame || !cancelAnimationFrame) {
requestAnimationFrame = function (callback) {
const currTime = new Date().getTime()
// 為了使setTimteout的盡可能的接近每秒60幀的效果
const timeToCall = Math.max(0, 16 - (currTime - lastTime))
const id = window.setTimeout(() => {
callback(currTime + timeToCall)
}, timeToCall)
lastTime = currTime + timeToCall
return id
}
cancelAnimationFrame = function (id) {
window.clearTimeout(id)
}
}
完整代碼:
requestAnimationFrame.js
let lastTime = 0
const prefixes = 'webkit moz ms o'.split(' ') // 各瀏覽器前綴
let requestAnimationFrame
let cancelAnimationFrame
// 判斷是否是服務(wù)器環(huán)境
const isServer = typeof window === 'undefined'
if (isServer) {
requestAnimationFrame = function () {
return
}
cancelAnimationFrame = function () {
return
}
} else {
requestAnimationFrame = window.requestAnimationFrame
cancelAnimationFrame = window.cancelAnimationFrame
let prefix
// 通過遍歷各瀏覽器前綴,來得到requestAnimationFrame和cancelAnimationFrame在當(dāng)前瀏覽器的實(shí)現(xiàn)形式
for (let i = 0; i < prefixes.length; i++) {
if (requestAnimationFrame && cancelAnimationFrame) { break }
prefix = prefixes[i]
requestAnimationFrame = requestAnimationFrame || window[prefix + 'RequestAnimationFrame']
cancelAnimationFrame = cancelAnimationFrame || window[prefix + 'CancelAnimationFrame'] || window[prefix + 'CancelRequestAnimationFrame']
}
// 如果當(dāng)前瀏覽器不支持requestAnimationFrame和cancelAnimationFrame,則會(huì)退到setTimeout
if (!requestAnimationFrame || !cancelAnimationFrame) {
requestAnimationFrame = function (callback) {
const currTime = new Date().getTime()
// 為了使setTimteout的盡可能的接近每秒60幀的效果
const timeToCall = Math.max(0, 16 - (currTime - lastTime))
const id = window.setTimeout(() => {
callback(currTime + timeToCall)
}, timeToCall)
lastTime = currTime + timeToCall
return id
}
cancelAnimationFrame = function (id) {
window.clearTimeout(id)
}
}
}
export { requestAnimationFrame, cancelAnimationFrame }
CountTo.vue組件思路
首先引入requestAnimationFrame.js,使用requestAnimationFrame方法接受count函數(shù),還需要格式化數(shù)字,進(jìn)行正則表達(dá)式轉(zhuǎn)換,返回我們想要的數(shù)據(jù)格式。
引入 import { requestAnimationFrame, cancelAnimationFrame } from './requestAnimationFrame.js'
需要接受的參數(shù):
const props = defineProps({
start: {
type: Number,
required: false,
default: 0
},
end: {
type: Number,
required: false,
default: 0
},
duration: {
type: Number,
required: false,
default: 5000
},
autoPlay: {
type: Boolean,
required: false,
default: true
},
decimals: {
type: Number,
required: false,
default: 0,
validator (value) {
return value >= 0
}
},
decimal: {
type: String,
required: false,
default: '.'
},
separator: {
type: String,
required: false,
default: ','
},
prefix: {
type: String,
required: false,
default: ''
},
suffix: {
type: String,
required: false,
default: ''
},
useEasing: {
type: Boolean,
required: false,
default: true
},
easingFn: {
type: Function,
default(t, b, c, d) {
return c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b;
}
}
})
啟動(dòng)數(shù)字動(dòng)效
const startCount = () => {
state.localStart = props.start
state.startTime = null
state.localDuration = props.duration
state.paused = false
state.rAF = requestAnimationFrame(count)
}
核心函數(shù),對(duì)數(shù)字進(jìn)行轉(zhuǎn)動(dòng)
if (!state.startTime) state.startTime = timestamp
state.timestamp = timestamp
const progress = timestamp - state.startTime
state.remaining = state.localDuration - progress
// 是否使用速度變化曲線
if (props.useEasing) {
if (stopCount.value) {
state.printVal = state.localStart - props.easingFn(progress, 0, state.localStart - props.end, state.localDuration)
} else {
state.printVal = props.easingFn(progress, state.localStart, props.end - state.localStart, state.localDuration)
}
} else {
if (stopCount.value) {
state.printVal = state.localStart - ((state.localStart - props.end) * (progress / state.localDuration))
} else {
state.printVal = state.localStart + (props.end - state.localStart) * (progress / state.localDuration)
}
}
if (stopCount.value) {
state.printVal = state.printVal < props.end ? props.end : state.printVal
} else {
state.printVal = state.printVal > props.end ? props.end : state.printVal
}
state.displayValue = formatNumber(state.printVal)
if (progress < state.localDuration) {
state.rAF = requestAnimationFrame(count)
} else {
emits('callback')
}
}
// 格式化數(shù)據(jù),返回想要展示的數(shù)據(jù)格式
const formatNumber = (val) => {
val = val.toFixed(props.default)
val += ''
const x = val.split('.')
let x1 = x[0]
const x2 = x.length > 1 ? props.decimal + x[1] : ''
const rgx = /(\d+)(\d{3})/
if (props.separator && !isNumber(props.separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + props.separator + '$2')
}
}
return props.prefix + x1 + x2 + props.suffix
}
取消動(dòng)效
// 組件銷毀時(shí)取消動(dòng)畫
onUnmounted(() => {
cancelAnimationFrame(state.rAF)
})
完整代碼
<template>
{{ state.displayValue }}
</template>
<script setup> // vue3.2新的語法糖, 編寫代碼更加簡(jiǎn)潔高效
import { onMounted, onUnmounted, reactive } from "@vue/runtime-core";
import { watch, computed } from 'vue';
import { requestAnimationFrame, cancelAnimationFrame } from './requestAnimationFrame.js'
// 定義父組件傳遞的參數(shù)
const props = defineProps({
start: {
type: Number,
required: false,
default: 0
},
end: {
type: Number,
required: false,
default: 0
},
duration: {
type: Number,
required: false,
default: 5000
},
autoPlay: {
type: Boolean,
required: false,
default: true
},
decimals: {
type: Number,
required: false,
default: 0,
validator (value) {
return value >= 0
}
},
decimal: {
type: String,
required: false,
default: '.'
},
separator: {
type: String,
required: false,
default: ','
},
prefix: {
type: String,
required: false,
default: ''
},
suffix: {
type: String,
required: false,
default: ''
},
useEasing: {
type: Boolean,
required: false,
default: true
},
easingFn: {
type: Function,
default(t, b, c, d) {
return c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b;
}
}
})
const isNumber = (val) => {
return !isNaN(parseFloat(val))
}
// 格式化數(shù)據(jù),返回想要展示的數(shù)據(jù)格式
const formatNumber = (val) => {
val = val.toFixed(props.default)
val += ''
const x = val.split('.')
let x1 = x[0]
const x2 = x.length > 1 ? props.decimal + x[1] : ''
const rgx = /(\d+)(\d{3})/
if (props.separator && !isNumber(props.separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + props.separator + '$2')
}
}
return props.prefix + x1 + x2 + props.suffix
}
// 相當(dāng)于vue2中的data中所定義的變量部分
const state = reactive({
localStart: props.start,
displayValue: formatNumber(props.start),
printVal: null,
paused: false,
localDuration: props.duration,
startTime: null,
timestamp: null,
remaining: null,
rAF: null
})
// 定義一個(gè)計(jì)算屬性,當(dāng)開始數(shù)字大于結(jié)束數(shù)字時(shí)返回true
const stopCount = computed(() => {
return props.start > props.end
})
// 定義父組件的自定義事件,子組件以觸發(fā)父組件的自定義事件
const emits = defineEmits(['onMountedcallback', 'callback'])
const startCount = () => {
state.localStart = props.start
state.startTime = null
state.localDuration = props.duration
state.paused = false
state.rAF = requestAnimationFrame(count)
}
watch(() => props.start, () => {
if (props.autoPlay) {
startCount()
}
})
watch(() => props.end, () => {
if (props.autoPlay) {
startCount()
}
})
// dom掛在完成后執(zhí)行一些操作
onMounted(() => {
if (props.autoPlay) {
startCount()
}
emits('onMountedcallback')
})
// 暫停計(jì)數(shù)
const pause = () => {
cancelAnimationFrame(state.rAF)
}
// 恢復(fù)計(jì)數(shù)
const resume = () => {
state.startTime = null
state.localDuration = +state.remaining
state.localStart = +state.printVal
requestAnimationFrame(count)
}
const pauseResume = () => {
if (state.paused) {
resume()
state.paused = false
} else {
pause()
state.paused = true
}
}
const reset = () => {
state.startTime = null
cancelAnimationFrame(state.rAF)
state.displayValue = formatNumber(props.start)
}
const count = (timestamp) => {
if (!state.startTime) state.startTime = timestamp
state.timestamp = timestamp
const progress = timestamp - state.startTime
state.remaining = state.localDuration - progress
// 是否使用速度變化曲線
if (props.useEasing) {
if (stopCount.value) {
state.printVal = state.localStart - props.easingFn(progress, 0, state.localStart - props.end, state.localDuration)
} else {
state.printVal = props.easingFn(progress, state.localStart, props.end - state.localStart, state.localDuration)
}
} else {
if (stopCount.value) {
state.printVal = state.localStart - ((state.localStart - props.end) * (progress / state.localDuration))
} else {
state.printVal = state.localStart + (props.end - state.localStart) * (progress / state.localDuration)
}
}
if (stopCount.value) {
state.printVal = state.printVal < props.end ? props.end : state.printVal
} else {
state.printVal = state.printVal > props.end ? props.end : state.printVal
}
state.displayValue = formatNumber(state.printVal)
if (progress < state.localDuration) {
state.rAF = requestAnimationFrame(count)
} else {
emits('callback')
}
}
// 組件銷毀時(shí)取消動(dòng)畫
onUnmounted(() => {
cancelAnimationFrame(state.rAF)
})
</script>
總結(jié)
自己封裝數(shù)字動(dòng)態(tài)效果需要注意各個(gè)瀏覽器直接的差異,手動(dòng)pollyfill,暴露出去的props參數(shù)需要有默認(rèn)值,數(shù)據(jù)的格式化可以才有正則表達(dá)式的方式,組件的驅(qū)動(dòng)必須是數(shù)據(jù)變化,根據(jù)數(shù)據(jù)來驅(qū)動(dòng)頁面渲染,防止頁面出現(xiàn)卡頓,不要強(qiáng)行操作dom,引入的組件可以全局配置,后續(xù)組件可以服用。


到此這篇關(guān)于vue3數(shù)據(jù)可視化實(shí)現(xiàn)數(shù)字滾動(dòng)特效的文章就介紹到這了,更多相關(guān)vue3數(shù)據(jù)可視化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue結(jié)合ElementUI實(shí)現(xiàn)數(shù)據(jù)請(qǐng)求和頁面跳轉(zhuǎn)功能(最新推薦)
我們?cè)赑roflie.vue實(shí)例中,有beforeRouteEnter、beforeRouteLeave兩個(gè)函數(shù)分別是進(jìn)入路由之前和離開路由之后,我們可以在這兩個(gè)函數(shù)之內(nèi)進(jìn)行數(shù)據(jù)的請(qǐng)求,下面給大家分享Vue結(jié)合ElementUI實(shí)現(xiàn)數(shù)據(jù)請(qǐng)求和頁面跳轉(zhuǎn)功能,感興趣的朋友一起看看吧2024-05-05
自定義input組件如何實(shí)現(xiàn)拖拽文件上傳
這篇文章主要介紹了自定義input組件如何實(shí)現(xiàn)拖拽文件上傳問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
使用vuex較為優(yōu)雅的實(shí)現(xiàn)一個(gè)購物車功能的示例代碼
這篇文章主要介紹了使用vuex較為優(yōu)雅的實(shí)現(xiàn)一個(gè)購物車功能的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12
vue修改數(shù)據(jù)的時(shí)候,表單值回顯不正確問題及解決
這篇文章主要介紹了vue修改數(shù)據(jù)的時(shí)候,表單值回顯不正確的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11
Vue.js實(shí)戰(zhàn)之組件的進(jìn)階
組件(Component)是 Vue.js 最強(qiáng)大的功能之一,之前的文章都只是用到了基本的封裝功能,這次將介紹一些更強(qiáng)大的擴(kuò)展。這篇文章主要介紹了Vue.js實(shí)戰(zhàn)之組件進(jìn)階的相關(guān)資料,需要的朋友們可以參考借鑒,下面來一起看看吧。2017-04-04
使用vue打包時(shí)vendor文件過大或者是app.js文件很大的問題
這篇文章主要介紹了使用vue打包時(shí)vendor文件過大或者是app.js文件很大問題的解決方法,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-06-06

