0


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

前言

vue3不支持vue-count-to插件,无法使用vue-count-to实现数字动效,数字自动分割,vue-count-to主要针对vue2使用,vue3按照会报错:

  1. TypeError: Cannot read properties of undefined (reading '_c')

的错误信息。这个时候我们只能自己封装一个CountTo组件实现数字动效。先来看效果图:
请添加图片描述

思路

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

文件目录

文件目录

使用示例

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

入口文件index.js

  1. const UILib = {
  2. install(Vue) {
  3. Vue.component('CountTo', CountTo)
  4. }
  5. }
  6. export default UILib

main.js使用

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

requestAnimationFrame.js思路

  1. 先判断是不是浏览器还是其他环境
  2. 如果是浏览器判断浏览器内核类型
  3. 如果浏览器不支持requestAnimationFrame,cancelAnimationFrame方法,改写setTimeout定时器
  4. 导出两个方法 requestAnimationFrame, cancelAnimationFrame
  1. 各个浏览器前缀:let prefixes = 'webkit moz ms o';
  2. 判断是不是浏览器:let isServe = typeof window == 'undefined';
  3. 增加各个浏览器前缀:
  4. let prefix;
  5. let requestAnimationFrame;
  6. let cancelAnimationFrame;
  7. // 通过遍历各浏览器前缀,来得到requestAnimationFrame和cancelAnimationFrame在当前浏览器的实现形式
  8. for (let i = 0; i < prefixes.length; i++) {
  9. if (requestAnimationFrame && cancelAnimationFrame) { break }
  10. prefix = prefixes[i]
  11. requestAnimationFrame = requestAnimationFrame || window[prefix + 'RequestAnimationFrame']
  12. cancelAnimationFrame = cancelAnimationFrame || window[prefix + 'CancelAnimationFrame'] || window[prefix + 'CancelRequestAnimationFrame']
  13. }
  14. //不支持使用setTimeout方式替换:模拟60帧的效果
  15. // 如果当前浏览器不支持requestAnimationFrame和cancelAnimationFrame,则会退到setTimeout
  16. if (!requestAnimationFrame || !cancelAnimationFrame) {
  17. requestAnimationFrame = function (callback) {
  18. const currTime = new Date().getTime()
  19. // 为了使setTimteout的尽可能的接近每秒60帧的效果
  20. const timeToCall = Math.max(0, 16 - (currTime - lastTime))
  21. const id = window.setTimeout(() => {
  22. callback(currTime + timeToCall)
  23. }, timeToCall)
  24. lastTime = currTime + timeToCall
  25. return id
  26. }
  27. cancelAnimationFrame = function (id) {
  28. window.clearTimeout(id)
  29. }
  30. }

完整代码:

requestAnimationFrame.js

  1. let lastTime = 0
  2. const prefixes = 'webkit moz ms o'.split(' ') // 各浏览器前缀
  3. let requestAnimationFrame
  4. let cancelAnimationFrame
  5. // 判断是否是服务器环境
  6. const isServer = typeof window === 'undefined'
  7. if (isServer) {
  8. requestAnimationFrame = function () {
  9. return
  10. }
  11. cancelAnimationFrame = function () {
  12. return
  13. }
  14. } else {
  15. requestAnimationFrame = window.requestAnimationFrame
  16. cancelAnimationFrame = window.cancelAnimationFrame
  17. let prefix
  18. // 通过遍历各浏览器前缀,来得到requestAnimationFrame和cancelAnimationFrame在当前浏览器的实现形式
  19. for (let i = 0; i < prefixes.length; i++) {
  20. if (requestAnimationFrame && cancelAnimationFrame) { break }
  21. prefix = prefixes[i]
  22. requestAnimationFrame = requestAnimationFrame || window[prefix + 'RequestAnimationFrame']
  23. cancelAnimationFrame = cancelAnimationFrame || window[prefix + 'CancelAnimationFrame'] || window[prefix + 'CancelRequestAnimationFrame']
  24. }
  25. // 如果当前浏览器不支持requestAnimationFrame和cancelAnimationFrame,则会退到setTimeout
  26. if (!requestAnimationFrame || !cancelAnimationFrame) {
  27. requestAnimationFrame = function (callback) {
  28. const currTime = new Date().getTime()
  29. // 为了使setTimteout的尽可能的接近每秒60帧的效果
  30. const timeToCall = Math.max(0, 16 - (currTime - lastTime))
  31. const id = window.setTimeout(() => {
  32. callback(currTime + timeToCall)
  33. }, timeToCall)
  34. lastTime = currTime + timeToCall
  35. return id
  36. }
  37. cancelAnimationFrame = function (id) {
  38. window.clearTimeout(id)
  39. }
  40. }
  41. }
  42. export { requestAnimationFrame, cancelAnimationFrame }

CountTo.vue组件思路

首先引入requestAnimationFrame.js,使用requestAnimationFrame方法接受count函数,还需要格式化数字,进行正则表达式转换,返回我们想要的数据格式。

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

需要接受的参数:

  1. const props = defineProps({
  2. start: {
  3. type: Number,
  4. required: false,
  5. default: 0
  6. },
  7. end: {
  8. type: Number,
  9. required: false,
  10. default: 0
  11. },
  12. duration: {
  13. type: Number,
  14. required: false,
  15. default: 5000
  16. },
  17. autoPlay: {
  18. type: Boolean,
  19. required: false,
  20. default: true
  21. },
  22. decimals: {
  23. type: Number,
  24. required: false,
  25. default: 0,
  26. validator (value) {
  27. return value >= 0
  28. }
  29. },
  30. decimal: {
  31. type: String,
  32. required: false,
  33. default: '.'
  34. },
  35. separator: {
  36. type: String,
  37. required: false,
  38. default: ','
  39. },
  40. prefix: {
  41. type: String,
  42. required: false,
  43. default: ''
  44. },
  45. suffix: {
  46. type: String,
  47. required: false,
  48. default: ''
  49. },
  50. useEasing: {
  51. type: Boolean,
  52. required: false,
  53. default: true
  54. },
  55. easingFn: {
  56. type: Function,
  57. default(t, b, c, d) {
  58. return c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b;
  59. }
  60. }
  61. })

启动数字动效

  1. const startCount = () => {
  2. state.localStart = props.start
  3. state.startTime = null
  4. state.localDuration = props.duration
  5. state.paused = false
  6. state.rAF = requestAnimationFrame(count)
  7. }

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

  1. if (!state.startTime) state.startTime = timestamp
  2. state.timestamp = timestamp
  3. const progress = timestamp - state.startTime
  4. state.remaining = state.localDuration - progress
  5. // 是否使用速度变化曲线
  6. if (props.useEasing) {
  7. if (stopCount.value) {
  8. state.printVal = state.localStart - props.easingFn(progress, 0, state.localStart - props.end, state.localDuration)
  9. } else {
  10. state.printVal = props.easingFn(progress, state.localStart, props.end - state.localStart, state.localDuration)
  11. }
  12. } else {
  13. if (stopCount.value) {
  14. state.printVal = state.localStart - ((state.localStart - props.end) * (progress / state.localDuration))
  15. } else {
  16. state.printVal = state.localStart + (props.end - state.localStart) * (progress / state.localDuration)
  17. }
  18. }
  19. if (stopCount.value) {
  20. state.printVal = state.printVal < props.end ? props.end : state.printVal
  21. } else {
  22. state.printVal = state.printVal > props.end ? props.end : state.printVal
  23. }
  24. state.displayValue = formatNumber(state.printVal)
  25. if (progress < state.localDuration) {
  26. state.rAF = requestAnimationFrame(count)
  27. } else {
  28. emits('callback')
  29. }
  30. }
  31. // 格式化数据,返回想要展示的数据格式
  32. const formatNumber = (val) => {
  33. val = val.toFixed(props.default)
  34. val += ''
  35. const x = val.split('.')
  36. let x1 = x[0]
  37. const x2 = x.length > 1 ? props.decimal + x[1] : ''
  38. const rgx = /(\d+)(\d{3})/
  39. if (props.separator && !isNumber(props.separator)) {
  40. while (rgx.test(x1)) {
  41. x1 = x1.replace(rgx, '$1' + props.separator + '$2')
  42. }
  43. }
  44. return props.prefix + x1 + x2 + props.suffix
  45. }

取消动效

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

完整代码

  1. <template>
  2. {{ state.displayValue }}
  3. </template>
  4. <script setup> // vue3.2新的语法糖, 编写代码更加简洁高效
  5. import { onMounted, onUnmounted, reactive } from "@vue/runtime-core";
  6. import { watch, computed } from 'vue';
  7. import { requestAnimationFrame, cancelAnimationFrame } from './requestAnimationFrame.js'
  8. // 定义父组件传递的参数
  9. const props = defineProps({
  10. start: {
  11. type: Number,
  12. required: false,
  13. default: 0
  14. },
  15. end: {
  16. type: Number,
  17. required: false,
  18. default: 0
  19. },
  20. duration: {
  21. type: Number,
  22. required: false,
  23. default: 5000
  24. },
  25. autoPlay: {
  26. type: Boolean,
  27. required: false,
  28. default: true
  29. },
  30. decimals: {
  31. type: Number,
  32. required: false,
  33. default: 0,
  34. validator (value) {
  35. return value >= 0
  36. }
  37. },
  38. decimal: {
  39. type: String,
  40. required: false,
  41. default: '.'
  42. },
  43. separator: {
  44. type: String,
  45. required: false,
  46. default: ','
  47. },
  48. prefix: {
  49. type: String,
  50. required: false,
  51. default: ''
  52. },
  53. suffix: {
  54. type: String,
  55. required: false,
  56. default: ''
  57. },
  58. useEasing: {
  59. type: Boolean,
  60. required: false,
  61. default: true
  62. },
  63. easingFn: {
  64. type: Function,
  65. default(t, b, c, d) {
  66. return c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b;
  67. }
  68. }
  69. })
  70. const isNumber = (val) => {
  71. return !isNaN(parseFloat(val))
  72. }
  73. // 格式化数据,返回想要展示的数据格式
  74. const formatNumber = (val) => {
  75. val = val.toFixed(props.default)
  76. val += ''
  77. const x = val.split('.')
  78. let x1 = x[0]
  79. const x2 = x.length > 1 ? props.decimal + x[1] : ''
  80. const rgx = /(\d+)(\d{3})/
  81. if (props.separator && !isNumber(props.separator)) {
  82. while (rgx.test(x1)) {
  83. x1 = x1.replace(rgx, '$1' + props.separator + '$2')
  84. }
  85. }
  86. return props.prefix + x1 + x2 + props.suffix
  87. }
  88. // 相当于vue2中的data中所定义的变量部分
  89. const state = reactive({
  90. localStart: props.start,
  91. displayValue: formatNumber(props.start),
  92. printVal: null,
  93. paused: false,
  94. localDuration: props.duration,
  95. startTime: null,
  96. timestamp: null,
  97. remaining: null,
  98. rAF: null
  99. })
  100. // 定义一个计算属性,当开始数字大于结束数字时返回true
  101. const stopCount = computed(() => {
  102. return props.start > props.end
  103. })
  104. // 定义父组件的自定义事件,子组件以触发父组件的自定义事件
  105. const emits = defineEmits(['onMountedcallback', 'callback'])
  106. const startCount = () => {
  107. state.localStart = props.start
  108. state.startTime = null
  109. state.localDuration = props.duration
  110. state.paused = false
  111. state.rAF = requestAnimationFrame(count)
  112. }
  113. watch(() => props.start, () => {
  114. if (props.autoPlay) {
  115. startCount()
  116. }
  117. })
  118. watch(() => props.end, () => {
  119. if (props.autoPlay) {
  120. startCount()
  121. }
  122. })
  123. // dom挂在完成后执行一些操作
  124. onMounted(() => {
  125. if (props.autoPlay) {
  126. startCount()
  127. }
  128. emits('onMountedcallback')
  129. })
  130. // 暂停计数
  131. const pause = () => {
  132. cancelAnimationFrame(state.rAF)
  133. }
  134. // 恢复计数
  135. const resume = () => {
  136. state.startTime = null
  137. state.localDuration = +state.remaining
  138. state.localStart = +state.printVal
  139. requestAnimationFrame(count)
  140. }
  141. const pauseResume = () => {
  142. if (state.paused) {
  143. resume()
  144. state.paused = false
  145. } else {
  146. pause()
  147. state.paused = true
  148. }
  149. }
  150. const reset = () => {
  151. state.startTime = null
  152. cancelAnimationFrame(state.rAF)
  153. state.displayValue = formatNumber(props.start)
  154. }
  155. const count = (timestamp) => {
  156. if (!state.startTime) state.startTime = timestamp
  157. state.timestamp = timestamp
  158. const progress = timestamp - state.startTime
  159. state.remaining = state.localDuration - progress
  160. // 是否使用速度变化曲线
  161. if (props.useEasing) {
  162. if (stopCount.value) {
  163. state.printVal = state.localStart - props.easingFn(progress, 0, state.localStart - props.end, state.localDuration)
  164. } else {
  165. state.printVal = props.easingFn(progress, state.localStart, props.end - state.localStart, state.localDuration)
  166. }
  167. } else {
  168. if (stopCount.value) {
  169. state.printVal = state.localStart - ((state.localStart - props.end) * (progress / state.localDuration))
  170. } else {
  171. state.printVal = state.localStart + (props.end - state.localStart) * (progress / state.localDuration)
  172. }
  173. }
  174. if (stopCount.value) {
  175. state.printVal = state.printVal < props.end ? props.end : state.printVal
  176. } else {
  177. state.printVal = state.printVal > props.end ? props.end : state.printVal
  178. }
  179. state.displayValue = formatNumber(state.printVal)
  180. if (progress < state.localDuration) {
  181. state.rAF = requestAnimationFrame(count)
  182. } else {
  183. emits('callback')
  184. }
  185. }
  186. // 组件销毁时取消动画
  187. onUnmounted(() => {
  188. cancelAnimationFrame(state.rAF)
  189. })
  190. </script>

总结

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

demo演示

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

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

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

本文转载自: https://blog.csdn.net/weixin_42974827/article/details/126831847
版权归原作者 KinHKin(五年前端) 所有, 如有侵权,请联系我们删除。

“vue3 | 数据可视化实现数字滚动特效”的评论:

还没有评论