本文探讨JavaScript中实现精准倒计时的方法。针对setTimeout和setInterval因单线程机制导致的延迟问题,介绍了两种解决方案:一是利用Web Worker开启独立线程执行计时逻辑,避免主线程阻塞;二是采用requestAnimationFrame方案,利用显示器刷新率同步更新,并结合performance.now()修正后台暂停带来的误差,确保计时准确与高性能。
JavaScript倒计时不准的原因分析
为什么使用setTimeout、setInterval做倒计时不准?
首要原因在于JavaScript的单线程执行机制。由于该语言一次仅能处理一个任务,当设置计时器时,其回调函数会被推入任务队列,等待事件循环调度。若计时器到期时调用栈中仍有其他任务在执行,回调函数的执行将被推迟,这种延迟直接导致计时结果不准确。
其次,浏览器对 setTimeout 和 setInterval 的最小时间间隔设有下限,通常不低于4毫秒,这进一步限制了计时的精度。此外,系统资源紧张(如CPU负载过高)也可能导致回调函数执行延迟,加剧计时误差。
明确了上述原因后,解决思路便清晰起来。
鉴于JavaScript的单线程特性,我们可以尝试开辟独立线程专门负责倒计时逻辑,此时Web Worker便派上用场。
基于Web Worker的独立线程方案
Web Worker方案
import { ref } from 'vue'
let countDownWorker: Worker | null = null
export default function useCountDownWorker(defaultTime: number) {
const seconds = ref(defaultTime)
function initCountDownWorker() {
if (!countDownWorker) {
countDownWorker = new Worker(
new URL('./count-down.work.js', import.meta.url),
{ type: 'module' }
)
countDownWorker.postMessage({ type: 'ready', data: defaultTime })
countDownWorker.addEventListener('message', function (event) {
const { type, data } = event.data
switch (type) {
case 'ready':
console.log(data)
break
case 'data':
seconds.value = data
break
case 'stop':
break
case 'end':
countDownWorker?.terminate()
countDownWorker = null
break
case 'reset':
seconds.value = data
break
}
})
countDownWorker.addEventListener('error', function (event) {
console.log(event)
})
}
}
function startCountDown() {
if (!countDownWorker) {
initCountDownWorker()
}
countDownWorker?.postMessage({ type: 'start' })
}
function stopCountDown() {
countDownWorker?.postMessage({ type: 'stop' })
}
function resetCountDown() {
countDownWorker?.postMessage({ type: 'reset', data: defaultTime })
}
return { seconds, startCountDown, stopCountDown, resetCountDown }
}
count-down.work.js
let seconds = 0
let interval = null
function countDown() {
if (interval) {
clearInterval(interval)
}
interval = setInterval(() => {
if (seconds <= 0) {
postMessage({ type: 'end' })
return
}
seconds--
postMessage({ type: 'data', data: seconds })
}, 1000)
}
function stop() {
clearInterval(interval)
postMessage({ type: 'stop' })
}
function reset(data) {
clearInterval(interval)
seconds = data
postMessage({ type: 'reset', data: seconds })
}
function ready() {
postMessage({ type: 'ready', data: 'it is ready' })
}
addEventListener('message', function (event) {
const { type, data } = event.data
switch (type) {
case 'ready':
seconds = data
ready()
break
case 'start':
countDown()
break
case 'stop':
stop()
break
case 'reset':
reset(data)
break
}
})基于requestAnimationFrame的高性能方案
requestAnimationFrame方案
除了使用独立线程外,另一种高效方案是利用requestAnimationFrame。该API的回调频率通常与显示器刷新率同步,能最大程度减少阻塞并优化性能。具体实现方式如下:
import { ref, type Ref } from 'vue'
let requestAnimationFrameId: number | null = null
export default function useCountDownRAF(defaultTime: number) {
const seconds = ref(defaultTime)
function countDown(seconds: Ref<number>) {
const end = performance.now() + seconds.value * 1000
const step = () => {
const now = performance.now()
const remaining = Math.max(0, end - now)
seconds.value = Math.round(remaining / 1000)
if (remaining > 0) {
requestAnimationFrameId = requestAnimationFrame(step)
} else {
seconds.value = 0
if (requestAnimationFrameId) {
cancelAnimationFrame(requestAnimationFrameId)
requestAnimationFrameId = null
}
}
}
requestAnimationFrame(step)
}
function startCountDown() {
if (seconds.value <= 0) {
seconds.value = defaultTime
}
countDown(seconds)
}
function stopCountDown() {
if (requestAnimationFrameId) {
cancelAnimationFrame(requestAnimationFrameId)
requestAnimationFrameId = null
}
}
function resetCountDown() {
seconds.value = defaultTime
stopCountDown()
}
return { seconds, startCountDown, stopCountDown, resetCountDown }
}注意:在使用requestAnimationFrame方案时,若页面被置于后台,回调函数会暂停执行以节省资源。因此,必须依赖performance.now()(优于Date,可避免系统时间被篡改的风险)或Date.now()来校正倒计时时间,确保准确性。
至此,关于JavaScript如何实现精准倒计时的介绍完毕。如需了解更多相关JavaScript倒计时内容,请搜索本站往期文章或继续浏览后续内容,感谢大家的支持!