前言
在前端开发中,我们经常使用 setTimeout 和 setInterval 来处理定时任务,例如轮询接口、动画帧、倒计时等。
但是,有一个常见问题:
当浏览器窗口非激活状态时(后台标签页) ,浏览器为了优化性能,会限制
setTimeout和setInterval的执行频率。这意味着你的定时任务可能被延迟,甚至停止执行。
为了解决这个问题,我们可以使用 Web Worker 来实现 不受页面激活状态影响的定时器。
本文以 workerTimer.setInterval 为例,详细介绍实现原理和使用方式。
一、为什么要用 Worker
浏览器对非激活标签页做了性能优化:
- Chrome 会将
setInterval的最小间隔限制到 1000ms(甚至更大) - Safari、Firefox 也有类似策略
后果:
- 精确计时任务不再准确
- 轮询接口可能被延迟
- 动画或倒计时出现卡顿
解决思路:
- Web Worker 独立于主线程
- 定时器在 Worker 内部运行,不受页面激活状态影响
- 主线程通过消息回调获取结果
二、workerTimer.setInterval 原理
workerTimer 封装了 Worker,实现了 setInterval 和 setTimeout 的高精度替代方案。
1. Worker 内部逻辑
- Worker 用
setInterval/setTimeout来触发定时器 - 将定时事件通过
postMessage发回主线程 - Worker 内维护自己的
intervalIds,可随时清理
javascript
const intervalId = setInterval(() => {
postMessage({
message: 'interval:tick',
id: data.id
})
}, data.interval)
- 每个定时器通过
id标识 - 主线程收到消息后,执行对应回调
2. workerTimer.setInterval 接口
typescript
workerTimer.setInterval(cb: () => void, interval: number, context?: any): number
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
| cb | function | 定时器回调函数 |
| interval | number | 时间间隔(毫秒) |
| context | any | 回调函数执行上下文(可选) |
返回值:
- 返回一个唯一
id,用于清理定时器
内部实现:
workerTimer自增id- 将回调和上下文保存到
callbacks - 通过
worker.postMessage启动 Worker 内的setInterval
kotlin
this.id++
const id = this.id
this.callbacks[id] = { fn: cb, context }
worker.postMessage({
command: 'interval:start',
interval,
id
})
return id
3. 回调处理
Worker 每次触发定时器,发送消息到主线程:
php
postMessage({
message: 'interval:tick',
id: data.id
})
主线程接收消息后:
kotlin
const callbackItem = this.callbacks[id]
if (callbackItem?.fn) callbackItem.fn.call(callbackItem.context)
- 找到对应的回调
- 用保存的上下文执行函数
- 保证了定时器逻辑与原生
setInterval一致
4. 清理定时器
bash
workerTimer.clearInterval(id)
- 发送消息到 Worker
- Worker 内部调用
clearInterval - 删除
callbacks中对应条目 - 避免内存泄漏
三、使用示例
javascript
import workerTimer from './workerTimer'
// 每秒打印一次
const timerId = workerTimer.setInterval(() => {
console.log('tick', new Date())
}, 1000)
// 5秒后停止
workerTimer.setTimeout(() => {
workerTimer.clearInterval(timerId)
console.log('interval cleared')
}, 5000)
特点:
- 在后台标签页也能保持高精度
- 回调函数可绑定自定义上下文
- 支持同时管理多个定时器
四、总结
- 浏览器后台标签页会降低原生定时器精度
- 使用 Worker 可以实现 高精度、独立于页面状态的定时器
workerTimer.setInterval封装了 Worker 内部逻辑,提供与原生 API 接近的接口- 可配合
workerTimer.setTimeout实现完整定时器功能
ts
// 定义Worker消息类型
interface WorkerMessage {
command: string
interval?: number
timeout?: number
id: number
}
interface WorkerResponse {
message: string
id: number
}
// 创建Web Worker
const blobURL = URL.createObjectURL(
new Blob(
[
'(',
function () {
const intervalIds: Record<number, number> = {}
self.onmessage = function onMsgFunc(e: MessageEvent) {
const data = e.data
switch (data.command) {
case 'interval:start': {
const intervalId = setInterval(() => {
postMessage({
message: 'interval:tick',
id: data.id
})
}, data.interval)
postMessage({
message: 'interval:started',
id: data.id
})
intervalIds[data.id] = intervalId as unknown as number
break
}
case 'interval:clear': {
clearInterval(intervalIds[data.id])
postMessage({
message: 'interval:cleared',
id: data.id
})
delete intervalIds[data.id]
break
}
case 'timeout:start': {
const timeoutId = setTimeout(() => {
postMessage({
message: 'timeout:tick',
id: data.id
})
postMessage({
message: 'timeout:cleared',
id: data.id
})
delete intervalIds[data.id]
}, data.timeout)
intervalIds[data.id] = timeoutId as unknown as number
break
}
case 'timeout:clear': {
clearTimeout(intervalIds[data.id])
postMessage({
message: 'timeout:cleared',
id: data.id
})
delete intervalIds[data.id]
break
}
}
}
}.toString(),
')()'
],
{ type: 'application/javascript' }
)
)
const worker = new Worker(blobURL)
URL.revokeObjectURL(blobURL)
type CallbackItem = {
fn: () => void
context?: any
}
const workerTimer = {
id: 0,
callbacks: {} as Record<number, CallbackItem>,
setInterval(cb: () => void, interval: number, context?: any): number {
this.id++
const id = this.id
this.callbacks[id] = { fn: cb, context }
worker.postMessage({
command: 'interval:start',
interval,
id
})
return id
},
setTimeout(cb: () => void, timeout: number, context?: any): number {
this.id++
const id = this.id
this.callbacks[id] = { fn: cb, context }
worker.postMessage({
command: 'timeout:start',
timeout,
id
})
return id
},
onMessage(e: MessageEvent<WorkerResponse>) {
const { message, id } = e.data
switch (message) {
case 'interval:tick':
case 'timeout:tick': {
const callbackItem = this.callbacks[id]
if (callbackItem?.fn) callbackItem.fn.call(callbackItem.context)
break
}
case 'interval:cleared':
case 'timeout:cleared':
delete this.callbacks[id]
break
}
},
clearInterval(id: number) {
worker.postMessage({ command: 'interval:clear', id })
},
clearTimeout(id: number) {
worker.postMessage({ command: 'timeout:clear', id })
}
}
worker.onmessage = workerTimer.onMessage.bind(workerTimer)
export default workerTimer