方法一:使用 Web Worker 保持精確計時
1. 創(chuàng)建 Worker 文件(timer-worker.js)
let intervalId = null;
self.addEventListener('message', (e) => {
const { type, interval } = e.data;
if (type === 'start') {
if (intervalId) clearInterval(intervalId);
intervalId = setInterval(() => {
self.postMessage('tick');
}, interval);
} else if (type === 'stop') {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
}
});
2. 在主線程中使用 Worker
const worker = new Worker('timer-worker.js');
worker.addEventListener('message', (e) => {
if (e.data === 'tick') {
console.log('定時任務(wù)執(zhí)行', new Date());
}
});
worker.postMessage({ type: 'start', interval: 1000 });
優(yōu)點:即使頁面最小化或切換到后臺,Worker 中的 setInterval 依然保持設(shè)定的頻率。
注意:Worker 中不能直接訪問 DOM,需要通過 postMessage 與主線程通信,因此適合執(zhí)行不直接操作頁面的邏輯(如數(shù)據(jù)輪詢、計時更新等)。
方法二:結(jié)合 Page Visibility API 動態(tài)調(diào)整策略
如果無法使用 Worker(例如需要頻繁操作 DOM),可以監(jiān)聽頁面的可見性變化,當頁面變?yōu)椴豢梢姇r,改用更寬松的策略,但無法徹底避免頻率限制。
let intervalId = null;
let isPageVisible = true;
document.addEventListener('visibilitychange', () => {
isPageVisible = !document.hidden;
if (isPageVisible) {
startTimer(1000);
} else {
}
});
function startTimer(interval) {
if (intervalId) clearInterval(intervalId);
intervalId = setInterval(() => {
console.log('任務(wù)執(zhí)行', new Date());
}, interval);
}
startTimer(1000);
局限:瀏覽器仍會限制后臺頁面的計時器頻率,因此無法真正“解決”變慢問題,只能根據(jù)場景適配。
方法三:使用 setTimeout 遞歸 + 時間補償
通過記錄實際執(zhí)行時間與預(yù)期時間的偏差,動態(tài)調(diào)整下一次 setTimeout 的延遲,可以在一定程度上緩解頻率降低帶來的累積誤差,但依然無法繞過瀏覽器的底層限制。
let expectedTime = 0;
let timeoutId = null;
function scheduleTask(interval) {
if (timeoutId) clearTimeout(timeoutId);
const now = Date.now();
if (expectedTime === 0) {
expectedTime = now + interval;
} else {
expectedTime += interval;
}
const delay = Math.max(0, expectedTime - now);
timeoutId = setTimeout(() => {
console.log('任務(wù)執(zhí)行', new Date());
scheduleTask(interval);
}, delay);
}
scheduleTask(1000);
說明:這種方法可以確保任務(wù)在后臺仍按設(shè)定的間隔執(zhí)行,但 setTimeout 同樣受瀏覽器限制(最小間隔通常為 1 秒),所以實際效果有限。
總結(jié)
選擇哪種方案取決于你的具體需求。
在 vue3中如何使用
1. 創(chuàng)建 Worker 文件
在 src/workers 目錄下創(chuàng)建 timer.worker.js:
let intervalId = null
self.addEventListener('message', (e) => {
const { type, interval } = e.data
if (type === 'start') {
if (intervalId) clearInterval(intervalId)
intervalId = setInterval(() => {
self.postMessage('tick')
}, interval)
} else if (type === 'stop') {
if (intervalId) {
clearInterval(intervalId)
intervalId = null
}
}
})
注意:如果使用 Vite,可以直接用 ?worker 后綴導(dǎo)入,也可以使用 new Worker(new URL(...)) 方式(推薦)。
2. 封裝一個組合式函數(shù)(Composable)
創(chuàng)建一個 useWorkerTimer.ts(或 .js):
import { ref, onMounted, onUnmounted } from 'vue'
export function useWorkerTimer(interval = 1000, autoStart = true) {
const worker = ref(null)
const tick = ref(0)
const isRunning = ref(false)
const initWorker = () => {
worker.value = new Worker(new URL('../workers/timer.worker.js', import.meta.url))
worker.value.addEventListener('message', (e) => {
if (e.data === 'tick') {
tick.value++
}
})
}
const start = () => {
if (!worker.value) initWorker()
worker.value?.postMessage({ type: 'start', interval })
isRunning.value = true
}
const stop = () => {
worker.value?.postMessage({ type: 'stop' })
isRunning.value = false
}
const terminate = () => {
stop()
if (worker.value) {
worker.value.terminate()
worker.value = null
}
}
onMounted(() => {
if (autoStart) start()
})
onUnmounted(() => {
terminate()
})
return {
tick,
isRunning,
start,
stop,
terminate
}
}
3. 在 Vue 組件中使用
<template>
<div>
<p>Worker 定時器已運行:{{ tick }} 次</p>
<button @click="start" :disabled="isRunning">啟動</button>
<button @click="stop" :disabled="!isRunning">停止</button>
</div>
</template>
<script setup>
import { useWorkerTimer } from '@/composables/useWorkerTimer'
const { tick, isRunning, start, stop } = useWorkerTimer(1000, true)
</script>
4. 進階:傳遞數(shù)據(jù)與主線程交互
如果需要在 Worker 中執(zhí)行更復(fù)雜的任務(wù)(例如發(fā)起網(wǎng)絡(luò)請求),可以通過 postMessage 傳遞數(shù)據(jù)。
Worker 端接收數(shù)據(jù)
self.addEventListener('message', async (e) => {
const { type, payload } = e.data
if (type === 'fetch') {
const res = await fetch(payload.url)
const data = await res.json()
self.postMessage({ type: 'fetchResult', data })
}
})
主線程發(fā)送并接收結(jié)果
worker.value?.postMessage({
type: 'fetch',
payload: { url: 'https://api.example.com/data' }
})
worker.value?.addEventListener('message', (e) => {
if (e.data.type === 'fetchResult') {
console.log('獲取到數(shù)據(jù):', e.data.data)
}
})
5. 注意事項
-
Worker 文件路徑
在 Vite 中,使用 new URL('../workers/timer.worker.js', import.meta.url) 可以保證開發(fā)和生產(chǎn)環(huán)境路徑正確。
如果使用 Vue CLI,可以簡單用 new Worker('@/workers/timer.worker.js'),但需要確保 Webpack 正確處理。
-
響應(yīng)式數(shù)據(jù)更新
通過 tick 的更新可以驅(qū)動視圖重新渲染,這是通過 Vue 的響應(yīng)式系統(tǒng)自動完成的。
-
生命周期清理
在組件卸載時,務(wù)必調(diào)用 worker.terminate() 避免內(nèi)存泄漏。上面封裝的 useWorkerTimer 已處理。
-
兼容性
Web Worker 支持現(xiàn)代瀏覽器及移動端,如果需要兼容非常古老的瀏覽器,可使用降級方案(如 fallback 到 setInterval)。
總結(jié)
在 Vue 3 中使用 Web Worker 保持精確計時,只需三步:
-
創(chuàng)建獨立的 Worker 文件,內(nèi)部使用 setInterval 并 postMessage 通知主線程。
-
封裝組合式函數(shù)管理 Worker 生命周期(創(chuàng)建、啟動、停止、銷毀)。
-
在組件中調(diào)用該函數(shù),即可享受不受頁面可見性影響的穩(wěn)定定時器。
這種方式非常適合輪詢、實時數(shù)據(jù)更新、倒計時等需要精確計時的業(yè)務(wù)場景。