Vue 封装一个倒计时组件
1. 创建一个基础组件
<template>
<div class="_base-count-down"></div>
</template>
<script>
export default {
name: 'index',
props: {},
data () {
return {}
}
}
</script>
<style lang="scss" scoped>
</style>
2. 实现基本的倒计时组件
...
<script>
export default {
name: 'index',
props: {
// 新增代码
time: {
type: [String, Number],
default: 0
},
// 新增代码
isMilliSecond: {
type: Boolean,
default: false
}
},
data () {
return {}
},
computed: {
// 新增代码
duration () {
return this.isMilliSecond ? Math.round(+this.time / 1000) : Math.round(+this.time)
}
}
}
</script>
...
...
<script>
export default {
...
// 新增代码
mounted () {
this.countDown()
},
// 新增代码
methods: {
countDown () {
this.getTime(this.duration)
}
}
}
</script>
...
<template>
<div class="_base-count-down">
<!-- 新增代码 -->
还剩{{days}}天{{hours}}:{{mins}}:{{seconds}}
</div>
</template>
<script>
export default {
...
props: {...},
// 新增代码
data () {
return {
days: '0',
hours: '00',
mins: '00',
seconds: '00',
timer: null
}
},
computed: {...},
mounted () {...},
methods: {
countDown () {
this.getTime(this.duration)
},
getTime (duration) {
// 执行时,将上一个setTimeout清除掉
this.timer && clearTimeout(this.timer)
if (duration < 0) return
const { dd, hh, mm, ss } = this.durationFormatter(duration)
this.days = dd || 0
this.hours = hh || 0
this.mins = mm || 0
this.seconds = ss || 0
this.timer = setTimeout(() => {
this.getTime(duration - 1)
}, 1000)
},
durationFormatter (time) {
if (!time) return { ss: 0 }
let t = time
const ss = t % 60
t = (t - ss) / 60
if (t < 1) return { ss }
const mm = t % 60
t = (t - mm) / 60
if (t < 1) return { mm, ss }
const hh = t % 24
t = (t - hh) / 24
if (t < 1) return { hh, mm, ss }
const dd = t
return { dd, hh, mm, ss }
}
}
}
</script>
...
3. setTimeout和setInterval使用比较
setTimeout(function(){···}, n); // n毫秒后执行function
setInterval(function(){···}, n); // 每隔n毫秒执行一次function
这里使用setInterval
不是更加方便吗?
强调:定时器指的是时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。倒置何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,再执行。
setInterval(function, N)
// 即:每隔N秒吧function事件推到消息队列中,并不是隔N秒后执行代码
setInterval
每隔N秒,会往消息队列中添加一个事件;- N秒后,添加a定时器代码至队列中,若主线程中还有其他任务在执行,所以需要等待;
- 当其他任务执行结束后执行a定时器代码 ;
- 又过了N秒,b定时器被添加到队列中,主线程还在执行a代码,所以等待;
- 又过了N秒,理论上又要往队列里推一个定时器代码,但由于此时b还在队列中,所以c不会被添加,结果就是此时c被跳过;
- 可以看到,a定时器执行结束后马上执行了b代码,所以并没有定时器的效果。
综上所述,
setInterval
会有两个问题:
- 使用
setInterval
时,某些间隔会被跳过;- 可能多个定时器会连续执行;
可以这样理解:每个
setTimeout
产生的任务会直接push
到任务队列中,而setInterval
在每次把任务push
到任务队列前,都要进行一下判断(看上一次的任务是否仍在队列中)。
因而一般用setTimeout
模拟setInterval
,来规避上面的问题。
4. 监听时间
如图,在倒计时的父组件中,有两个按钮,活动一和活动二,点击传入相应的时间。
若此时的倒计时组件正在做活动一的倒计时,再点击活动二,会马上传入新的time,此时应该需要重新计时。
但组件的
mounted
只会执行一次,则this.getTime(this.duration)
,只会执行一次,因此duration
还是活动一的时间。通过
watch
监听duration
,当发现变化时,说明新的时间time传入组件,然后重新调用this.countDown()
...
<script>
export default {
name: 'index',
props: {...},
data () {...},
computed: {...},
// 新增代码
watch: {
duration () {
this.countDown()
}
},
mounted () {...},
methods: {...}
}
</script>
...
5. 使用diffTime
注意:此时的组件只是一个基础的组件,若到了线上会有一个bug。当页面打开时,倒计时开始,时间是
还剩1天12:25:25
,切换应用后,一段时间再切回浏览器,发现倒计时还是还剩1天12:25:25
。出于性能的考虑,部分浏览器再进入后台或失去焦点时,会将
setTimeout
等定时任务暂停(或者说是延迟),待用户切回时,才能重新激活定时任务。需要改写一下
getTime
方法。
diffTime
:表示当前这个setTimeout
的回调函数执行的时刻距离上一个setTimeout
的回调函数执行的时刻时间段。
...
<script>
export default {
name: 'index',
props: {...},
data () {
return {
days: '0',
hours: '00',
mins: '00',
seconds: '00',
timer: null,
// 新增代码
curTime: 0
}
},
computed: {...},
watch: {...},
mounted () {...},
methods: {
countDown () {
// 新增代码
this.curTime = Date.now()
this.getTime(this.duration)
},
getTime (duration) {
this.timer && clearTimeout(this.timer)
if (duration < 0) return
const { dd, hh, mm, ss } = this.durationFormatter(duration)
this.days = dd || 0
this.hours = hh || 0
this.mins = mm || 0
this.seconds = ss || 0
this.timer = setTimeout(() => {
// 新增代码
const now = Date.now()
const diffTime = Math.floor((now - this.curTime) / 1000)
this.curTime = now
this.getTime(duration - diffTime)
}, 1000)
},
durationFormatter (time) {...}
}
}
</script>
...
6. 添加功能
改动一下duration
就好
...
<script>
export default {
name: 'index',
props: {
...
// 新增代码
end: {
type: [String, Number],
default: 0
}
},
data () {...},
computed: {
duration () {
// 新增代码
if (this.end) {
let end = String(this.end).length >= 13 ? +this.end : +this.end * 1000
end -= Date.now()
return end
}
return this.isMilliSecond ? Math.round(+this.time / 1000) : Math.round(+this.time)
}
},
watch: {...},
mounted () {...},
methods: {...}
}
</script>
...
只需改动html
<template>
<div class="_base-count-down">
<div class="content">
<slot v-bind="{
d: days, h: hours, m: mins, s:seconds,
hh: `00${hours}`.slice(-2),
mm: `00${mins}`.slice(-2),
ss: `00${seconds}`.slice(-2)
}"></slot>
</div>
</div>
</template>
父组件使用
<template>
<div class="subunit">
<base-counter :time="countDownTime" v-slot="timeObj" >
<div class="count-down">
<div class="icon"></div>
{{timeObj.d}}天{{timeObj.hh}}小时{{timeObj.mm}}分钟{{timeObj.ss}}秒
</div>
</base-counter>
</div>
</template>
<script>
import BaseCounter from './components/CountDown.vue'
export default {
components: { BaseCounter },
name: 'index',
data: () => ({
countDownTime: 2 * 24 * 60 * 60
})
}
</script>
<style lang="scss" scoped>
</style>
7. 完整代码
<template>
<div class="_base-count-down">
<div class="content">
<slot v-bind="{
d: days, h: hours, m: mins, s:seconds,
hh: `00${hours}`.slice(-2),
mm: `00${mins}`.slice(-2),
ss: `00${seconds}`.slice(-2)
}"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'index',
props: {
time: {
type: [String, Number],
default: 0
},
isMilliSecond: {
type: Boolean,
default: false
},
end: {
type: [String, Number],
default: 0
}
},
data () {
return {
days: '0',
hours: '00',
mins: '00',
seconds: '00',
timer: null,
curTime: 0
}
},
computed: {
duration () {
if (this.end) {
let end = String(this.end).length >= 13 ? +this.end : +this.end * 1000
end -= Date.now()
return end
}
return this.isMilliSecond ? Math.round(+this.time / 1000) : Math.round(+this.time)
}
},
watch: {
duration () {
this.countDown()
}
},
mounted () {
this.countDown()
},
methods: {
countDown () {
this.curTime = Date.now()
this.getTime(this.duration)
},
getTime (duration) {
this.timer && clearTimeout(this.timer)
if (duration < 0) return
const { dd, hh, mm, ss } = this.durationFormatter(duration)
this.days = dd || 0
this.hours = hh || 0
this.mins = mm || 0
this.seconds = ss || 0
this.timer = setTimeout(() => {
const now = Date.now()
const diffTime = Math.floor((now - this.curTime) / 1000)
this.curTime = now
this.getTime(duration - diffTime)
}, 1000)
},
durationFormatter (time) {
if (!time) return { ss: 0 }
let t = time
const ss = t % 60
t = (t - ss) / 60
if (t < 1) return { ss }
const mm = t % 60
t = (t - mm) / 60
if (t < 1) return { mm, ss }
const hh = t % 24
t = (t - hh) / 24
if (t < 1) return { hh, mm, ss }
const dd = t
return { dd, hh, mm, ss }
}
}
}
</script>
<style lang="scss" scoped>
</style>