背景說明
我們在項目中經(jīng)常遇到定時器的使用舆绎,比如倒計時哟沫,當(dāng)我們切換瀏覽器頁面時,會發(fā)現(xiàn)倒計時不準確了或者 會有秒數(shù)從40 直接跳躍到30的場景,這是為什么呢压彭?
其實會出現(xiàn)這種情況是因為網(wǎng)頁失去焦點時,主瀏覽器對這些定時器的執(zhí)行頻率進行了限制辱匿,降低至每秒一次灶泵,這就導(dǎo)致了不準確的問題,如何解決呢补箍?
解決方案
worker-timers解決了以上問題改执,它可以保證在非活動窗口下也保持原有頻率倒計時。它的核心思想在于將定時器任務(wù)交由Web Worker處理坑雅,而Web Worker不受瀏覽器窗口失焦點的節(jié)流限制辈挂,它能夠依然按照原有頻率執(zhí)行代碼,確保了任務(wù)的準時執(zhí)行裹粤。
應(yīng)用場景
游戲邏輯計算终蒂、實時數(shù)據(jù)刷新、定時推送服務(wù)等遥诉,均能確保數(shù)據(jù)的準確性
倒計時案例
- 安裝 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) {
// 由于刪除該計時器時可能存在該計時器已經(jīng)入棧拇泣,所以要先清除掉,防止添加的時候重復(fù)計時
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 }, // 結(jié)束時間
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 }
},
// 恢復(fù)
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() })
},
// 結(jié)束回調(diào)
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')">恢復(fù)</el-button>
<el-button @click="changeEnd">變更結(jié)束時間</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>
-
實際效果
image.png
摘抄自:https://blog.csdn.net/gitblog_00561/article/details/141294756