数字的滚动的实现思路
vue本身就是根据数据来驱动view层的显示,实现数字的滚动本质就是设置一个延迟函数改变数据的同时,view层的显示也会随着改变达到渐变的效果。
组件化
为了考虑多种使用场景,将滚动抽离成组件,需要用到的属性
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
tag | 标签名 | String | 'span' |
start | 是否开始 | Boolean | true |
startVal | 起始值 | Number / String | 0 |
endVal | 结束值 | Number /String | - |
decimals | 几位小数 | Number | 2 |
duration | 过渡时间 | Number | 2 (s) |
isRestart | 是否可以暂停 | Boolean | false |
所以我们props的类型校验如下
// index.vue
<script>
import CountUp from './countup.js'
export default {
name: 'countup',
mounted() {
this.$nextTick(() => {
this._countup = new CountUp(
this.$el,
this.startVal,
this.endVal,
this.decimals,
this.duration
)
if (this.start) {
this._countup.init()
}
})
},
props: {
tag: {
type: String,
default: 'span'
},
start: {
type: Boolean,
default: true
},
startVal: {
type: Number | String,
default: 0
},
endVal: {
type: Number | String,
required: true
},
decimals: {
type: Number,
default: 2
},
duration: {
type: Number,
default: 2
},
isRestart: {
type: Boolean,
default: false
}
},
data() {
return {
times: 0
}
},
methods: {
onPauseResumeClick() {
if (this.isRestart) {
if (this.times === 0) {
this._countup.pauseResume()
this.times++
} else {
this._countup.restart()
this.times = 0
}
}
}
},
render(h) {
return h(
this.tag,
{
on: {
click: this.onPauseResumeClick
}
},
[this.startVal]
)
},
watch: {
start(val) {
if (val) {
this._countup.init()
}
},
endVal(val) {
this._countup.updateNew(this.endVal)
}
}
}
</script>
复制代码
逻辑部分抽离出来放在 countup.js文件中
。首先来看看index.vue 文件,在mounted中实例化了一个CountUp类,并且向这个类中传递了我们props接收到的参数
。并且在初始化和start值发生改变的时候
触发类中的init函
数,在endVal改变
的时候触发类的updateNew函数
。最终通过render函数将值渲染在view层。分析完index.vue文件后,好奇到底countup.js定义了哪些函数,接下来看下数字过渡的实现。
CountUp类
首先我们来看代码结构,暂时不关心细节做了什么,constructor构造函数
中接收到外部传入的值,并且将这些值添加到实例对象上。这样类上的方法(也就是类的prototype原型上的方法
)都可以通过this
访问到实例的对象的值。
class CountUp {
constructor(target, startVal, endVal, decimals, duration) {
this.target = target
this.startVal = startVal
this.endVal = endVal
this.decimals = decimals
this.duration = Number(this.duration) * 1000 || 2000
}
// 初始化
init() {
// 拿到DOM
this.label =
typeof this.target === 'string'
? document.getElementById(this.target)
: this.target
this.startVal = Number(this.startVal)
this.endVal = Number(this.endVal)
this.frameVal = this.startVal
this.startTime = new Date()
this.progress = this.endVal - this.frameVal
this.update()
}
// 更新
update() {
this.rAF = setInterval(() => {
const time = new Date() - this.startTime
const speed =
((new Date() - this.startTime) / this.duration) * this.progress
if (time >= this.duration) {
clearInterval(this.rAF)
this.frameVal = this.endVal
this.startVal = this.frameVal
} else {
this.frameVal = this.startVal + speed
}
this.printValue(this.frameVal)
})
}
// 打印值
printValue(value) {
this.label.innerHTML = value.toFixed(this.decimals)
}
// 有新的结束值
updateNew(newEndVal) {
this.pauseResume()
this.endVal = newEndVal
this.init()
}
// 暂停
pauseResume() {
clearInterval(this.rAF)
this.startVal = this.frameVal
}
// 重新开始
restart() {
this.init()
}
}
export default CountUp
复制代码
constructor
构造函数中拿到数据,然后通过各个prototype
上的方法如:printValue(打印值)、updateNew(更新)......
实现代码逻辑。有了对这个类结构的认识,我们来看看每个模块都做了什么事。
在mounted钩子中我们通过this._countup.init()
初始化,在初始化过程中主要做了一些安全转换,判断传入的$el如果未字符串则获取对应id的DOM,否则将target本身就是DOM,将起始值和结束值都转为数字类型,关键点开启计时
设置startTime,我们后面会通过时间来判断是否已经达到目标值用来判断是否停止过渡,计算出总的路程的绝对值
。在初始化的结束时开启执行下一个execute
函数。
过渡
init函数中最重要的就是设置了过渡的开始时间,计算出起始值到结束值总的路程。接下来就是数字滚动的过渡过程。
update() {
this.rAF = setInterval(() => {
const time = new Date() - this.startTime
const speed =
((new Date() - this.startTime) / this.duration) * this.progress
if (time >= this.duration) {
clearInterval(this.rAF)
this.frameVal = this.endVal
this.startVal = this.frameVal
} else {
this.frameVal = this.startVal + speed
}
this.printValue(this.frameVal)
})
}
复制代码
在update更新函数
中我们设置一个setInterval重复执行数字的累计过程
,通过单位时间/总时间*路程=速度
的公式来累计,要注意的是speed本身是有正负
的所以不需要考虑是加还是减的问题。并且我们通过printValue
函数将每次更新的值更新到DOM节点上。并且在这个函数中控制DOM的显示,如 toFixed来控制数字显示的小数点位数,当然也可以控制整数部分每三位加一个,
的显示如:10,200
// 打印值
printValue(value) {
this.label.innerHTML = value.toFixed(this.decimals)
}
复制代码
至此我们已经完成了数字滚动过渡功能,来看看制作的效果吧。
以为大功告成了,结果发现在我们更新结束值5000在未达到时又更改为500会瞬间改回来。 // 有新的结束值
updateNew(newEndVal) {
this.pauseResume()
this.endVal = newEndVal
this.init()
}
// 暂停
pauseResume() {
clearInterval(this.rAF)
this.startVal = this.frameVal
}
复制代码
我们需要在更新endVal之前将上一个的定时器清除掉
,否则会一直使用通一个setInterval 。所以在500 -> 5000 的中途我们将值改为500相当于startVal和endVal都是500自然不会又过渡效果,并且会立即返回500的值。加上了pauseResume函数后再来看过渡效果。
isRestart为true是否开启可暂停模式
,在为真的情况下判断
点击次数times
为0时暂停,为1时重新开始滚动。
methods: {
onPauseResumeClick() {
if (this.isRestart) {
if (this.times === 0) {
this._countup.pauseResume()
this.times++
} else {
this._countup.restart()
this.times = 0
}
}
}
},
render(h) {
return h(
this.tag,
{
on: {
click: this.onPauseResumeClick
}
},
[this.startVal]
)
},
复制代码
巧用 v-if v-show 完成卡到千卡单位的转换
// number1
<span v-if="isComplate">
<count :start-val="1"
:end-val="formatConsume"></count>千卡
</span>
// number2
<span v-show="!isComplate">
<count :start-val="0"
:end-val="1000"></count>卡
</span>
复制代码
通过v-if重新渲染和v-show显示隐藏的机制
,isComplate是用来判断是否已经达到1000,这里用v-if来控制number1来重新渲染,如果这里用v-show则页面进入的时候就会开始加载过渡效果
。不是我们想要的效果。之所以number2要用v-show是将其隐藏掉,如果是v-if直接消失在DOM会再触发transition的过渡效果
,过渡将变成500->5000->500的效果
,我们只需要将其隐藏掉同时显示number1的过渡效果即可。
结语
我们完成了数字过渡的组件,首先通过index.vue的prop接受参数,将逻辑部分放在countup.js中通过引入后实例化这个类。在初始化和更新值的时候调用类中的方法达到更新DOM的效果。下节将分享圆环加载的过渡效果
。