背景说明
我们在项目中经常遇到定时器的使用,比如倒计时,当我们切换浏览器页面时,会发现倒计时不准确了或者 会有秒数从40 直接跳跃到30的场景,这是为什么呢?
其实会出现这种情况是因为网页失去焦点时,主浏览器对这些定时器的执行频率进行了限制,降低至每秒一次,这就导致了不准确的问题,如何解决呢?
解决方案
worker-timers解决了以上问题,它可以保证在非活动窗口下也保持原有频率倒计时。它的核心思想在于将定时器任务交由Web Worker处理,而Web Worker不受浏览器窗口失焦点的节流限制,它能够依然按照原有频率执行代码,确保了任务的准时执行。
应用场景
游戏逻辑计算、实时数据刷新、定时推送服务等,均能确保数据的准确性
倒计时案例
- 安装 npm install worker-timers dayjs
- 公共方法utils
// utils.timer.js
import { clearTimeout, setTimeout } from 'worker-timers';
class Timer {
timerList = [];
addTimer (name, callback, time = 1000) {
this.timerList.push({
name,
callback,
time
});
this.runTimer(name);
}
static runTimer (name) {
const _this = this;
(function inner () {
const task = _this.timerList.find((item) => {
return item.name === name;
});
if (!task) return;
task.t = setTimeout(() => {
task.callback();
clearTimeout(task.t);
inner();
}, task.time);
})();
}
clearTimer (name) {
const taskIndex = this.timerList.findIndex((item) => {
return item.name === name;
});
if (taskIndex !== -1) {
// 由于删除该计时器时可能存在该计时器已经入栈,所以要先清除掉,防止添加的时候重复计时
clearTimeout(this.timerList[taskIndex].t);
this.timerList.splice(taskIndex, 1);
}
}
}
export default new Timer();
- 封装倒计时组件
// CountDown.vue 组件
<template>
<div class="countdown">
<slot name="time" :timeObject="timeObject"></slot>
<div v-if="!$scopedSlots.time">
<span class="letter" v-for="(letter, i) of display" :key="i">{{ letter }}</span>
</div>
</div>
</template>
<script>
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import duration from 'dayjs/plugin/duration'
import timer from '@/utils/timer.js';
dayjs.extend(utc)
dayjs.extend(duration)
export default {
name: 'CountDown',
props: {
time: { type: [Date, Number, dayjs], default: () => Date.now() }, // 开始时间
end: { type: [Number, String, Date], required: true }, // 结束时间
format: { type: String, default: 'HH : mm : ss' } // 格式
},
data () {
return {
now: 0,
intervalId: Symbol('statTimer'),
endTime: null,
isPause: false // 暂停否
}
},
computed: {
duration () {
return dayjs.duration(this.remain * 1000)
},
remain () {
let number = ''
this.endTime = this.endTime + ''
if (this.now) {
if (this.endTime.length == 10) {
number = this.endTime - this.now <= 0 ? 0 : this.endTime - this.now
} else if (this.endTime.length == 13) {
number = this.endTime / 1000 - this.now <= 0 ? 0 : this.endTime / 1000 - this.now
}
}
return number
},
months () { return this.duration.months() },
weeks () { return this.duration.weeks() },
days () { return this.duration.days() },
hours () { return this.duration.hours() },
minutes () { return this.duration.minutes() },
seconds () { return this.duration.seconds() },
count () { return this.remain >= 1 },
years () { return this.duration.years() },
display () { return this.duration.format(this.format) },
timeObject () {
if (this.months == 0 && this.weeks == 0 && this.days == 0 && this.hours == 0 && this.minutes == 0 && this.seconds == 0 && this.years == 0) {
this.timeEnd()
}
return {
formatTime: this.display, // 时间段
months: this.fixedNumber(this.months),
weeks: this.weeks,
days: this.fixedNumber(this.days),
hours: this.fixedNumber(this.hours),
minutes: this.fixedNumber(this.minutes),
seconds: this.fixedNumber(this.seconds),
years: this.fixedNumber(this.years),
}
}
},
mounted () {
},
methods: {
getTimeInfo () {
return { ...this.timeObject, end: this.end, time: this.time }
},
// 恢复
recover () {
this.isPause = false
timer.clearTimer(this.intervalId)
timer.addTimer(this.intervalId, () => { this.now++ }, 1000)
this.$emit('countDownCallback', { type: 'recover', value: this.getTimeInfo() })
},
// 暂停
pause () {
this.isPause = true
timer.clearTimer(this.intervalId)
this.$emit('countDownCallback', { type: 'pause', value: this.getTimeInfo() })
},
// 结束回调
timeEnd () {
this.$emit('countDownCallback', {
type: 'timeEnd',
})
},
// 补零
fixedNumber (number) {
number += ''
return number.length == 2 ? number : '0' + number
}
},
watch: {
time: {
immediate: true,
handler (n) {
if (n && !this.isPause) {
this.now = this.time / 1000
}
}
},
end: {
immediate: true,
handler (n) {
this.endTime = Number(n)
}
},
count: {
handler (v) {
if (v) timer.addTimer(this.intervalId, () => { this.now++ }, 1000)
else timer.clearTimer(this.intervalId)
},
immediate: true
}
},
destroyed () { timer.clearTimer(this.intervalId) }
}
</script>
<style scoped>
.letter {
display: inline-block;
white-space: pre;
}
</style>
- 组件使用方法
<template>
<div class=''>
<countdown ref="countdown" @countDownCallback="countDownCallback" :end="endTime" :time="Date.now()" format="DD[天] HH[时] mm[分] ss[秒]">
<!-- <template #time="{ timeObject }">
<div>
{{ timeObject }}
</div>
</template> -->
</countdown>
<el-button @click="pause">暂停</el-button>
<el-button @click="recover('continue')">恢复</el-button>
<el-button @click="changeEnd">变更结束时间</el-button>
</div>
</template>
<script>
import dayjs from 'dayjs'
import Countdown from './Countdown.vue'
export default {
name: 'CountDownDemo',
components: { Countdown },
props: {},
data () {
return {
dayjs,
endTime: dayjs('2024-08-23 16:16:00').valueOf()
}
},
methods: {
changeEnd () {
this.endTime = dayjs('2024-08-24 16:18:00').valueOf()
},
pause () {
this.$refs.countdown.pause()
},
recover () {
this.$refs.countdown.recover()
},
countDownCallback ({ type, value }) {
console.log('value: ', value);
console.log('type: ', type);
}
}
}
</script>
-
实际效果
摘抄自:https://blog.csdn.net/gitblog_00561/article/details/141294756