组件接收参数
名称 | 类型 | 描述 |
---|---|---|
time | Number|String | 传入的时间 |
传入不同的类型的值
- T1. Number:是一个
正常的时间戳
(秒/毫秒) - T2. Number:是一个
大于0的正整数的值
(计时秒数) - T3. String:是一个
过期(到期)时间
值 | 类型 | 所对应的场景 |
---|---|---|
1704961397 | Number | T1 |
1704961397896 | Number | T1 |
200 (3分20秒) | Number | T2 |
7234 (2时34秒) | Number | T2 |
2024-01-11 16:20:34 | String | T3 |
判断是否是一个有效的日期
function isValid (time) {
return !isNaN( new Date(time) )
}
isValid('2024-01-11 16:20:34') // true
isValid('abc') // false
isValid('记得要点赞哟') // false
如果不是一个有效期时间,开发阶段直接抛出异常,如下图
定义组件(components/countDown.vue)
统一值的处理:不管传入的值是一个
毫秒时间戳
、时间戳
、具体的时间日期
、计时秒数
,使用计算属性computed
统一转化为计时秒数
(场景T1
、T3
统一转为T2
)
<template>
<div class="__countdown-container">
<slot v-bind="result"></slot>
</div>
</template>
<script>
export default {
props: {
time: { type: [Number, String], default: 0 },
},
data() {
return {
result: { day: 0, hour: 0, minute: 0, second: 0 },
timer: null,
}
},
computed: {
duration() {
if (!this.isValid()) return
let t = this.time
if (Number.isInteger(t)) {
if (String(t).length < 10) return t
if (String(t).length === 10) t = +t * 1e3
}
return new Date(t).getTime() - Date.now()
},
},
mounted() {
this.startCountDown()
},
watch: {
duration() {
this.startCountDown()
},
},
methods: {
/**
* 判断是否是一个有效的时间类型
* @return true 有效时间
*/
isValid() {
if (Number.isInteger(this.time)) return !0
return !isNaN(new Date(this.time))
},
/**
* 开启计时器
*/
startCountDown() {
// 如果不是一个有效的时间类型,new Date(time)返回是一个NaN,则不往下执行
if (!this.isValid()) {
throw new Error('time is invalid')
}
this.duration && this.getTime(this.duration)
},
getTime(duration) {
this.timer && clearTimeout(this.timer)
if (duration < 0) return
this.result = this.formater(duration - 1)
duration--
this.timer = setTimeout(() => {
this.getTime(duration - 1)
}, 1e3)
},
formater(duration) {
let h, i, s
if (!duration) s = 0
let t = duration
;(s = t % 60), (t = (t - s) / 60)
;(i = t % 60), (t = (t - i) / 60)
;(h = t % 24), (t = (t - h) / 24)
return { day: t, hour: h, minute: i, second: s }
},
},
}
</script>
父组件(views/demo/index.vue)中调用
<template>
<div>
<div class="title">倒计时组件</div>
<div class="sub-select">
<select v-model="selectTime">
<option v-for="o in options" :key="o.title" :value="o.value">
{{ o.title }}
</option>
</select>
<span suffix>后过期</span>
</div>
<div class="sub-submit">
<button type="button" @click="setCouterTime">开始计时</button>
</div>
<count-down v-if="time" v-slot="res" :time="time">
<div class="count-down">
剩余<strong>{{ fillZero(res.day) }}</strong
>天<strong>{{ fillZero(res.hour) }}</strong
>时<strong>{{ fillZero(res.minute) }}</strong
>分<strong>{{ fillZero(res.second) }}</strong
>秒
</div>
</count-down>
</div>
</template>
<script setup>
const selectTime = ref(180)
const time = ref('')
const options = [
{ title: '不是一个有效的值', value: '不是一个有效的值' },
{ title: '3分钟', value: 180 },
{ title: '2小时', value: 7200 },
{ title: '1天', value: 86400 },
{ title: '3天', value: 3 * 86400 },
{ title: '1周', value: 7 * 86400 },
{ title: '1个月', value: 30 * 86400 },
{ title: '3个月', value: 90 * 86400 },
{ title: '6个月', value: 182 * 86400 },
{ title: '1年', value: 365 * 86400 },
]
const fillZero = computed(() => val => {
if (String(val).length > 2) return val
return `00${val}`.slice(-2)
})
// 设置倒计时时间
const setCouterTime = () => (time.value = selectTime.value)
onMounted(setCouterTime)
onUnmounted(() => {
time.value = ''
})
</script>
<style lang="scss" scoped>
.title {
text-align: center;
font-size: 2rem;
}
input,
select {
border: none;
width: 100%;
height: 36px;
padding: 0 0.5rem;
border-radius: 4px;
}
.sub-input,
.sub-select,
.sub-submit {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
margin-top: 1rem;
padding: 0 1rem;
border: none;
select {
border-radius: 4px 0 0 4px;
}
span[suffix] {
width: 100px;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
color: #000;
height: 36px;
line-height: 36px;
border-radius: 0 4px 4px 0;
}
}
.count-down {
width: 80%;
margin: 1rem auto;
display: flex;
justify-content: center;
font-size: 1.5rem;
}
strong {
font-family: monospace;
color: #fff;
text-align: center;
margin: 0 0.2rem;
padding: 0 0.25rem;
border-radius: 4px;
background-color: #9163f5;
}
button {
width: 100%;
height: 36px;
font-weight: 600;
padding: 0 1rem;
border-radius: 4px;
border: none;
transition: all 0.3s ease-in-out;
color: #fff;
background-color: #9163f5;
cursor: pointer;
&:hover {
background-color: rgba(145, 99, 245, 0.8);
}
}
</style>
为什么使用setTimeout来模拟setInterval的计时行为
强调,定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行。
上图可见,setInterval每隔100ms往队列中添加一个事件;
100ms后,添加T1定时器代码至队列中,主线程中还有任务在执行,所以等待,some event执行结束后执行T1定时器代码;
又过了100ms,T2定时器被添加到队列中,主线程还在执行T1代码,所以等待;
又过了100ms,理论上又要往队列里推一个定时器代码,但由于此时T2还在队列中,所以T3不会被添加,结果就是此时被跳过;
这里我们可以看到,T1定时器执行结束后马上执行了T2代码,所以并没有达到定时器的效果。
综上所述,setInterval有两个缺点:
- 使用setInterval时,某些间隔会被跳过;
- 可能多个定时器会连续执行;
可以这么理解:每个setTimeout产生的任务会直接push到任务队列中;而setInterval在每次把任务push到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中) 。
因而我们一般用setTimeout模拟setInterval,来规避掉上面的缺点。