需要从父级获取的数据
time
: 当前倒计时的剩余时间,传秒或毫秒isMilliSecond
: 用来判断当前的传入值是秒还是毫秒值end
: 用来传入具体的终点时间,传入秒级时间戳或毫秒级时间戳format
: 用来控制最终的显示格式,默认格式'D天HH时MM分SS秒'
flag
: 用来判断,是否在最高值为0时,不显示最高值
js
// countDown.vue
<script setup lang="ts">
const props = defineProps({
time: {
type: [Number, String],
default: 0,
},
isMilliSecond: {
type: Boolean,
default: false,
},
end: {
type: [Number, String],
default: 0,
},
format: {
type: String,
default: () => 'D天HH时MM分SS秒',
},
flag: {
type: Boolean,
default: false,
}
})
</script>
<template>
<div class="count_down">
{{ timeStr }}
</div>
</template>
基础变量
curTime
: 存储当前时间,因为当浏览器退至后台时,会将setTimeout
等定时任务暂停,通过curTime
用以更新倒计时days
,hours
,mins
,seconds
: 倒计时的各个部分<想着总不能超过一年倒计时吧>timer
: 存储定时器remainingTime
: 计算倒计时的秒数timeStr
: 格式化时间字符串
js
// countDown.vue
<script setup lang="ts">
import { computed, onMounted, ref, watch, type Ref } from 'vue';
const props = defineProps({
time: {
type: [Number, String],
default: 0,
},
isMilliSecond: {
type: Boolean,
default: false,
},
end: {
type: [Number, String],
default: 0,
},
format: {
type: String,
default: () => 'D天HH时MM分SS秒',
},
flag: {
type: Boolean,
default: false,
}
})
let curTime = 0
const days: Ref<string | number> = ref('0')
const hours: Ref<string | number> = ref('00')
const mins: Ref<string | number> = ref('00')
const seconds: Ref<string | number> = ref('00')
let timer: any = null;
const remainingTime = computed(() => {
if(props.end) {
let end = props.isMilliSecond ? +props.end : +props.end * 1000;
end -= Date.now();
return Math.round(end / 1000);
}
const time = props.isMilliSecond ? Math.round(+props.time / 1000) : Math.round(+props.time)
return time
})
const timeStr = computed(() => {
const o: {
[key: string]: any
} = {
'D+': days.value,
'H+': hours.value,
'M+': mins.value,
'S+': seconds.value,
}
let str = props.format;
// 当最高值为0时,去除值及其单位,有缺陷,只能去除对应目标前的所有字段
if(days.value == 0 && props.flag) {
let regexPattern = /.*(?=H)/;
if(hours.value == 0) {
regexPattern = /.*(?=M)/;
if(mins.value == 0) {
regexPattern = /.*(?=S)/;
}
}
str = str.replace(regexPattern, '');
}
for (var k in o) {
// 括号的目的是将占位符的模式 k 捕获到一个分组中,以便在替换字符串中的占位符时能够引用它。
str = str.replace(new RegExp(`(${k})`, 'g'), function(match, group) {
let time = group.length === 1 ? o[k] : `00${o[k]}`.slice(-group.length);
// 如果是天数,不管是什么格式,都把天数显示完整,但如果多个D,会在小于10之前加0
if(k == 'D+' && group.length > 1) {
time = o[k];
if(time < 10) {
time = `0${time}`
}
}
return time
});
}
return str;
})
</script>
<template>
<div class="count_down">
{{ timeStr }}
</div>
</template>
基础方法
countDown
: 进入页面后立即执行countDown
,并执行countdown
,从而开始倒计时formatTime
: 将remainingTime
转化成天数,小时,分钟,秒数的方法countdown
: 获取时间后开始倒计时的执行,
js
// countDown.vue
<script setup lang="ts">
const countDown = () => {
curTime = Date.now()
countdown(remainingTime.value)
}
const formatTime = (time: number) => {
const secondsInMinute = 60;
const secondsInHour = 24;
let t = time;
let ss = t % secondsInMinute;
t = (t - ss) / secondsInMinute;
const mm = t % secondsInMinute;
t = (t - mm) / secondsInMinute;
const hh = t % secondsInHour;
t = (t - hh) / secondsInHour;
const dd = t % secondsInHour;
return { dd, hh, mm, ss };
}
const countdown = (time: number) => {
timer && clearTimeout(timer)
if(time < 0) {
return;
}
const { dd, hh, mm, ss } = formatTime(time);
days.value = dd || 0;
hours.value = hh || 0;
mins.value = mm || 0;
seconds.value = ss || 0;
timer = setTimeout(() => {
const now = Date.now();
const diffTime = Math.floor((now - curTime) / 1000)
const step = diffTime > 1 ? diffTime : 1; // 页面退到后台的时候不会计时,对比时间差,大于1s的重置倒计时
curTime = now;
countdown(time - step);
}, 1000);
}
onMounted(() => {
countDown();
})
</script>
为什么不使用setInterval来实现
- 间隔不准确:
setInterval
的间隔并不保证准确,因为它只是将回调函数添加到消息队列,实际执行时间依赖于主线程的负载和事件循环,可能会被跳过或累积多次执行。 - 堆积问题: 如果一个
setInterval
回调执行的时间比其间隔短,那么它会叠加执行。这可能会导致不必要的资源消耗和不符合设计预期的行为。
这些问题通常是由于 JavaScript
的单线程执行和事件循环机制导致的。在实际开发中,为了更准确地处理定时任务,通常会使用 setTimeout
和递归或计算属性来处理定时任务。 虽然 setInterval
有一些局限性,但在某些情况下它仍然可以派上用场,特别是对于一些简单的定时操作。但在需要更精确的定时和依赖于前后状态的场景中,通常会选择使用 setTimeout
或其他更高级的定时管理方法。
完整代码
js
// countDown.vue
<script setup lang="ts">
import { computed, onMounted, ref, watch, type Ref } from 'vue';
const props = defineProps({
time: {
type: [Number, String],
default: 0,
},
isMilliSecond: {
type: Boolean,
default: false,
},
end: {
type: [Number, String],
default: 0,
},
format: {
type: String,
default: () => 'D天HH时MM分SS秒',
},
flag: {
type: Boolean,
default: false,
}
})
let curTime = 0
const days: Ref<string | number> = ref('0')
const hours: Ref<string | number> = ref('00')
const mins: Ref<string | number> = ref('00')
const seconds: Ref<string | number> = ref('00')
let timer: any = null;
const remainingTime = computed(() => {
if(props.end) {
let end = props.isMilliSecond ? +props.end : +props.end * 1000;
end -= Date.now();
return Math.round(end / 1000);
}
const time = props.isMilliSecond ? Math.round(+props.time / 1000) : Math.round(+props.time)
return time
})
const timeStr = computed(() => {
const o: {
[key: string]: any
} = {
'D+': days.value,
'H+': hours.value,
'M+': mins.value,
'S+': seconds.value,
}
let str = props.format;
// 如果天数为0的情况,希望去掉H之前的部分
if(days.value == 0 && props.flag) {
let regexPattern = /.*(?=H)/;
if(hours.value == 0) {
regexPattern = /.*(?=M)/;
if(mins.value == 0) {
regexPattern = /.*(?=S)/;
}
}
str = str.replace(regexPattern, '');
}
for (var k in o) {
// 括号的目的是将占位符的模式 k 捕获到一个分组中,以便在替换字符串中的占位符时能够引用它。
str = str.replace(new RegExp(`(${k})`, 'g'), function(match, group) {
let time = group.length === 1 ? o[k] : `00${o[k]}`.slice(-group.length);
if(k == 'D+' && group.length > 1) {
time = o[k];
if(time < 10) {
time = `0${time}`
}
}
return time
});
}
return str;
})
const countDown = () => {
curTime = Date.now()
countdown(remainingTime.value)
}
const formatTime = (time: number) => {
const secondsInMinute = 60;
const secondsInHour = 24;
let t = time;
let ss = t % secondsInMinute;
t = (t - ss) / secondsInMinute;
const mm = t % secondsInMinute;
t = (t - mm) / secondsInMinute;
const hh = t % secondsInHour;
t = (t - hh) / secondsInHour;
const dd = t % secondsInHour;
return { dd, hh, mm, ss };
}
const countdown = (time: number) => {
timer && clearTimeout(timer)
if(time < 0) {
return;
}
const { dd, hh, mm, ss } = formatTime(time);
days.value = dd || 0;
hours.value = hh || 0;
mins.value = mm || 0;
seconds.value = ss || 0;
timer = setTimeout(() => {
const now = Date.now();
const diffTime = Math.floor((now - curTime) / 1000)
const step = diffTime > 1 ? diffTime : 1; // 页面退到后台的时候不会计时,对比时间差,大于1s的重置倒计时
curTime = now;
countdown(time - step);
}, 1000);
}
watch(remainingTime, () => {
countDown()
}, { immediate: true })
onMounted(() => {
countDown();
})
</script>
<template>
<div class="count_down">
{{ timeStr }}
</div>
</template>
js
// 父级调用
<script setup lang="ts">
import countDown from './components/countDown.vue';
</script>
<template>
<div id="app">
<count-down
:end="1698980400000"
:is-milli-second="true"
:flag="true"
/>
</div>
</template>
弊端
虽然这样能够通过父级传入的格式进行对应的显示,但是这样的同时,无法对每个单元的内容或者样式进行调整,也无法根据父级来动态显示不同的样式 想法: 可以通过插槽的方式,将值传递给父级,通过父级来控制显示的内容
调整之后的代码:基本代码无调整,通过插槽将值 会传给父级
js
// countDown.vue
<script setup lang="ts">
import { computed, onMounted, ref, watch, type Ref } from 'vue';
const props = defineProps({
time: {
type: [Number, String],
default: 0,
},
isMilliSecond: {
type: Boolean,
default: false,
},
end: {
type: [Number, String],
default: 0,
},
})
let curTime = 0
const days: Ref<string | number> = ref('0')
const hours: Ref<string | number> = ref('00')
const mins: Ref<string | number> = ref('00')
const seconds: Ref<string | number> = ref('00')
let timer: any = null;
const remainingTime = computed(() => {
if(props.end) {
let end = props.isMilliSecond ? +props.end : +props.end * 1000;
end -= Date.now();
return Math.round(end / 1000);
}
const time = props.isMilliSecond ? Math.round(+props.time / 1000) : Math.round(+props.time)
return time
})
const countDown = () => {
curTime = Date.now()
countdown(remainingTime.value)
}
const formatTime = (time: number) => {
const secondsInMinute = 60;
const secondsInHour = 24;
let t = time;
let ss = t % secondsInMinute;
t = (t - ss) / secondsInMinute;
const mm = t % secondsInMinute;
t = (t - mm) / secondsInMinute;
const hh = t % secondsInHour;
t = (t - hh) / secondsInHour;
const dd = t % secondsInHour;
return { dd, hh, mm, ss };
}
const countdown = (time: number) => {
timer && clearTimeout(timer)
if(time < 0) {
return;
}
const { dd, hh, mm, ss } = formatTime(time);
days.value = dd || 0;
hours.value = hh || 0;
mins.value = mm || 0;
seconds.value = ss || 0;
timer = setTimeout(() => {
const now = Date.now();
const diffTime = Math.floor((now - curTime) / 1000)
const step = diffTime > 1 ? diffTime : 1; // 页面退到后台的时候不会计时,对比时间差,大于1s的重置倒计时
curTime = now;
countdown(time - step);
}, 1000);
}
watch(remainingTime, () => {
countDown()
}, { immediate: true })
onMounted(() => {
countDown();
})
</script>
<template>
<div class="count_down">
<slot v-bind="{
d: days, h: hours, m: mins, s: seconds,
dd: `00${days}`.slice(-2),
hh: `00${hours}`.slice(-2),
mm: `00${mins}`.slice(-2),
ss: `00${seconds}`.slice(-2),
}"></slot>
</div>
</template>
js
// 父级调用
<script setup lang="ts">
import countDown from './components/countDown.vue';
</script>
<template>
<div id="app">
<count-down v-slot="timeObj" :end="1698980400000" :is-milli-second="true">
{{timeObj.d}}天{{timeObj.hh}}小时{{timeObj.mm}}分钟{{timeObj.ss}}秒
</count-down>
</div>
</template>