当前位置:首页 » 《随便一记》 » 正文

vue3 | 数据可视化实现数字滚动特效

6 人参与  2022年09月17日 19:06  分类 : 《随便一记》  评论

点击全文阅读


前言

vue3不支持vue-count-to插件,无法使用vue-count-to实现数字动效,数字自动分割,vue-count-to主要针对vue2使用,vue3按照会报错:
TypeError: Cannot read properties of undefined (reading '_c')
的错误信息。这个时候我们只能自己封装一个CountTo组件实现数字动效。先来看效果图:
请添加图片描述

思路

使用Vue.component定义公共组件,使用window.requestAnimationFrame(首选,次选setTimeout)来循环数字动画,window.cancelAnimationFrame取消数字动画效果,封装一个requestAnimationFrame.js公共文件,CountTo.vue组件,入口导出文件index.js。

文件目录

文件目录

使用示例

<CountTo :start="0" // 从数字多少开始 :end="endCount" // 到数字多少结束 :autoPlay="true" // 自动播放 :duration="3000" // 过渡时间 prefix="¥"   // 前缀符号 suffix="rmb" // 后缀符号 />

入口文件index.js

const UILib = {  install(Vue) {    Vue.component('CountTo', CountTo)  }}export default UILib

main.js使用

import CountTo from './components/count-to/index';app.use(CountTo)

requestAnimationFrame.js思路

先判断是不是浏览器还是其他环境如果是浏览器判断浏览器内核类型如果浏览器不支持requestAnimationFrame,cancelAnimationFrame方法,改写setTimeout定时器导出两个方法 requestAnimationFrame, cancelAnimationFrame
各个浏览器前缀:let prefixes =  'webkit moz ms o';判断是不是浏览器:let isServe = typeof window == 'undefined';增加各个浏览器前缀:  let prefix;let requestAnimationFrame;let cancelAnimationFrame;// 通过遍历各浏览器前缀,来得到requestAnimationFrame和cancelAnimationFrame在当前浏览器的实现形式    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帧的效果  // 如果当前浏览器不支持requestAnimationFrame和cancelAnimationFrame,则会退到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 = 0const prefixes = 'webkit moz ms o'.split(' ') // 各浏览器前缀let requestAnimationFramelet cancelAnimationFrame// 判断是否是服务器环境const isServer = typeof window === 'undefined'if (isServer) {    requestAnimationFrame = function () {        return    }    cancelAnimationFrame = function () {        return    }} else {    requestAnimationFrame = window.requestAnimationFrame    cancelAnimationFrame = window.cancelAnimationFrame    let prefix    // 通过遍历各浏览器前缀,来得到requestAnimationFrame和cancelAnimationFrame在当前浏览器的实现形式    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']    }    // 如果当前浏览器不支持requestAnimationFrame和cancelAnimationFrame,则会退到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函数,还需要格式化数字,进行正则表达式转换,返回我们想要的数据格式。

引入 import { requestAnimationFrame, cancelAnimationFrame } from './requestAnimationFrame.js'

需要接受的参数:

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 startCount = () => {  state.localStart = props.start  state.startTime = null  state.localDuration = props.duration  state.paused = false  state.rAF = requestAnimationFrame(count)}

核心函数,对数字进行转动

  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')  }}// 格式化数据,返回想要展示的数据格式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}

取消动效

// 组件销毁时取消动画onUnmounted(() => {  cancelAnimationFrame(state.rAF)})

完整代码

<template>  {{ state.displayValue }}</template><script setup>  // vue3.2新的语法糖, 编写代码更加简洁高效import { onMounted, onUnmounted, reactive } from "@vue/runtime-core";import { watch, computed } from 'vue';import { requestAnimationFrame, cancelAnimationFrame } from './requestAnimationFrame.js'// 定义父组件传递的参数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))}// 格式化数据,返回想要展示的数据格式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}// 相当于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})// 定义一个计算属性,当开始数字大于结束数字时返回trueconst stopCount = computed(() => {  return props.start > props.end})// 定义父组件的自定义事件,子组件以触发父组件的自定义事件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挂在完成后执行一些操作onMounted(() => {  if (props.autoPlay) {    startCount()  }  emits('onMountedcallback')})// 暂停计数const pause = () => {  cancelAnimationFrame(state.rAF)}// 恢复计数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')  }}// 组件销毁时取消动画onUnmounted(() => {  cancelAnimationFrame(state.rAF)})</script>

总结

自己封装数字动态效果需要注意各个浏览器直接的差异,手动pollyfill,暴露出去的props参数需要有默认值,数据的格式化可以才有正则表达式的方式,组件的驱动必须是数据变化,根据数据来驱动页面渲染,防止页面出现卡顿,不要强行操作dom,引入的组件可以全局配置,后续组件可以服用,码字不易,请各位看官大佬多多支持,一键三连了~❤️❤️❤️

demo演示

后续的线上demo演示会放在
demo演示
完整代码会放在
个人主页

希望对vue开发者有所帮助~

个人简介:承吾工作年限:5年前端地区:上海个人宣言:立志出好文,传播我所会的,有好东西就及时与大家共享!
请添加图片描述
请添加图片描述

点击全文阅读


本文链接:http://zhangshiyu.com/post/44970.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1