Vue3+TS写个数字滚动效果CountTo组件

2,620 阅读4分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

前言

  最近开发有个需求需要酷炫的文字滚动效果,发现vue2版本的CountTo组件不适用与Vue3,没有轮子咋办,那咱造一个呗。其实大多数版本更替导致公共组件不可用,最简单的做法就是在原版本的基础上进行修改调整,总体来讲花费的时间成本以及精力成本最低。

思考

number.gif

  先看下效果,明确需求,然后开始搬砖。

明确基础功能

  • 有开始值、结束值以及动画持续时间
  • 默认分隔符、自动播放

扩展功能

  • 自动播放可配置
  • 分隔符可自定义
  • 前、后缀
  • 动画配置项

实践

定义参数

const props = {
  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
    }
  }
}

定义一个开始函数

    // 定义一个计算属性,当开始数字大于结束数字时返回true
    const stopCount = computed(() => {
      return props.start > props.end
    })
    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()
      }
      emit('onMountedcallback')
    })
     // 组件销毁时取消动画
    onUnmounted(() => {
      cancelAnimationFrame(state.rAF)
    })

核心方法

    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 {
        emit('callback')
      }
    }

配置项

属性描述类型默认值
startVal开始值Number0
endVal结束值Number0
duration持续时间Number0
autoplay自动播放Booleantrue
decimals要显示的小数位数Number0
decimal十进制分割String,
separator分隔符String,
prefix前缀String''
suffix后缀String''
useEasing使用缓和功能Booleantrue
easingFn缓和回调Function-

注:当autoplay:true时,它将在startVal或endVal更改时自动启动

功能

函数名描述
mountedCallback挂载以后返回回调
start开始计数
pause暂停计数
reset重置countTo

组件

组件同步在git组件库了github.com/kinoaa/kino…

import {
  defineComponent, reactive, computed, onMounted, watch, onUnmounted
} from 'vue'
const props = {
  start: {
    type: Number,
    required: false,
    default: 0
  },
  end: {
    type: Number,
    required: false,
    default: 2022
  },
  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
    }
  }
}
export default defineComponent({
  name: 'CountTo',
  props: props,
  emits: ['onMountedcallback', 'callback'],
  setup(props, {emit}) {
    const isNumber = (val) => {
      return !isNaN(parseFloat(val))
    }
    // 格式化数据,返回想要展示的数据格式
    const formatNumber = (val) => {
      if (val % 1 !== 0)val = val.toFixed(props.decimals)
      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
    }
    const state = reactive<{
      localStart: number
      displayValue: number|string
      printVal: any
      paused: boolean
      localDuration: any
      startTime: any
      timestamp: any
      remaining: any
      rAF: any
    }>({
      localStart: props.start,
      displayValue: formatNumber(props.start),
      printVal: null,
      paused: false,
      localDuration: props.duration,
      startTime: null,
      timestamp: null,
      remaining: null,
      rAF: null
    })
    // 定义一个计算属性,当开始数字大于结束数字时返回true
    const stopCount = computed(() => {
      return props.start > props.end
    })
    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()
      }
      emit('onMountedcallback')
    })
    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 {
        emit('callback')
      }
    }
    // 组件销毁时取消动画
    onUnmounted(() => {
      cancelAnimationFrame(state.rAF)
    })
    return () => (
      state.displayValue
    )
  }
})

往期精彩回顾

image.png

学会这些鲜有人知的coding技巧,从此早早下班liao-JavaScript实战技巧篇

image.png 前端图片最优化压缩方案

image.png Vue实战技巧Element Table二次封装

写在最后

我是凉城a,一个前端,热爱技术也热爱生活。

与你相逢,我很开心。

如果你想了解更多,请点这里,期待你的小⭐⭐

  • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊
  • 本文首发于掘金,未经许可禁止转载💌