前言
这是一道经常考的面试题。在实际业务开发中,也时常遇见类似的场景,至此做一个总结
应用场景
- 轮询,站内消息
- 时间线任务,时钟,每日任务线
- 前端监控数据上报
- 扫码登录
- 倒计时
实现方式
- setTimeout
- setInterval
- requestAnimationFrame
- 时间线,diffTime
我们这里主要讲解使用时间线,diffTime的方式
为什么不用setTimeout和setInterval?
因为setTimeout和setInterval不准时,无法精确计算
1、通过setTimeout实现
const serial = ['Red', 'Yellow', 'Green']
const times = [3, 10, 6]
function delay(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true)
}, time);
})
}
async function run() {
this.count = 0
while (1) {
const index = this.count % serial.length
const time = times[index]
const single = serial[index]
await delay(time * 1000)
console.log('当前信号为:', single)
this.count++
}
}
run()
通过延时函数,while循环+ 异步函数同步调用实现,加入count计数
效果为:
2、通过setInterval实现
const serial = ['Red', 'Yellow', 'Green']
const times = [3, 10, 6]
class TimerClass {
count = 0
singleTime = times[0]
single = serial[0]
timer = null
singleRemainder = 0
constructor() {
this.setTImer()
}
// 切换信号
changeSingle() {
this.timer && clearInterval(this.timer)
this.timer = null
const index = this.count % serial.length
const time = times[index]
const single = serial[index]
this.count++
this.single = single
this.singleRemainder = time
return { single, time }
}
// 设置定时器
setTImer() {
const that = this
if (!this.timer) {
that.timer = setInterval(() => {
console.log('剩余时间', that.singleRemainder)
if (that.singleRemainder <= 0) {
const { single, time } = that.changeSingle()
console.log('切换信号====>', single, time)
that.setTImer()
}
that.singleRemainder--
}, 1000);
} else {
that.changeSingle()
}
}
}
const timer = new TimerClass()
效果为:
1、setTimeout 和 serInterval 的区别
- serInterval : 定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行。如果执行代码执行时长超出下一个setInterval时间间隔,那么下一个setInterval 将会被忽略
setInterval有两个缺点:
- 使用setInterval时,某些间隔会被跳过;
- 可能多个定时器会连续执行;
- setTimeout:每个setTimeout产生的任务会直接push到任务队列中
1、当页面长时间休眠或浏览器处于非活动状态时,setTimeout的定时器会暂停执行,这是为了减少浏览器的缓存占用以及电脑的内存占用。当页面被重新激活后,定时器会被同步激活,从而继续执行。但这种执行是基于定时器休眠之前的参数继续执行,而不是以最新的环境参数执行,这可能会导致一些问题1。此外,在未激活的页面中,浏览器通常会对定时器的执行间隔进行调整,以降低对系统资源的占用。在大多数浏览器中,未激活的页面中的setTimeout最小间隔通常被调整为1000毫秒(1秒),这意味着如果你在未激活的页面中使用setTimeout,设置的时间间隔小于1000毫秒时,浏览器可能会延长执行时间间隔,
2、最小延迟时间:在浏览器中,setTimeout()/setInterval()的每调用一次定时器的最小间隔是4ms,这通常是由于函数嵌套导致或已经执行的setInterval的回调函数阻塞导致的。
3、系统负载:当系统负载较重时,事件循环可能会出现延迟,导致setTimeout的回调函数执行的时间比预期的要晚。
4、睡眠模式:在某些设备上,当设备进入睡眠模式时,定时器可能会暂停,直到设备被唤醒。这会导致setTimeout的回调函数执行时间延迟。
2、解决计算时间不精确的方案
- 使用Web Workers:将计时任务移至Web Workers中执行,避免与主线程的其他代码竞争,提高定时器的准确性。
- 使用时间戳:使用时间戳来进行倒计时和计算,可以更精确地控制时间。
- 手动调整:在每次定时器触发后,通过记录实际执行时间并与预期执行时间进行比较,计算误差并进行手动调整。
- 使用精确计时库,如requestAnimationFrame来实现准确的定时任务。
使用时间线计时
// 红黄绿灯
const serial = ['Red', 'Yellow', 'Green']
function delay(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true)
}, time);
})
}
class Single {
constructor(options) {
this.sig = options.init
this.times = options.times
this.setTime()
this.exchange()
}
// 时间计数, 时间线
setTime() {
this.start = Date.now()
const time = this.times[serial.indexOf(this.sig)]
this.end = this.start + time * 1000
}
// 下一个信号
get next() {
return serial[(serial.indexOf(this.sig) + 1) % serial.length]
}
// 剩余时间
get remain() {
let diff = this.end - Date.now()
if (diff <= 0) {
diff = 0
}
return diff / 1000
}
// 切换红绿灯
async exchange() {
if (this.remain > 0) {
// 存在剩余时间不切换
console.log(`当期信号灯【${this.sig}】,剩余时间【${Math.round(this.remain)}】`)
// 我们希望每一秒钟提示一次
await delay(1 * 1000)
} else {
this.sig = this.next
this.setTime()
console.log(`切换信号灯【${this.sig}】`)
}
this.exchange()
}
}
const s = new Single({
init: 'Green',
times: [10, 3, 5]
})
我们可以实时获取,当前信号以及剩余时间
效果为:
加入发布订阅模式/停止/重启
// 红黄绿灯
// 红黄绿灯
const serial = ['Red', 'Yellow', 'Green']
function delay(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true)
}, time);
})
}
class Single {
// 是否立即停止
isStop = false
constructor(options) {
this.sig = options.init
this.times = options.times
// 发布订阅模式
this.eventMap = new Map()
this.eventMap.set('tick', new Set([]))
this.eventMap.set('change', new Set([]))
this.setTime()
this.exchange()
}
on(event, handler) {
this.eventMap.get(event).add(handler)
}
off(event, handler) {
this.eventMap.get(event).delete(handler)
}
emit(event) {
this.eventMap.get(event).forEach(h => {
h.call(this, this)
});
}
// 时间计数, 时间线
setTime() {
this.start = Date.now()
const time = this.times[serial.indexOf(this.sig)]
this.end = this.start + time * 1000
}
// 下一个信号
get next() {
return serial[(serial.indexOf(this.sig) + 1) % serial.length]
}
// 剩余时间
get remain() {
let diff = this.end - Date.now()
if (diff <= 0) {
diff = 0
}
return diff / 1000
}
// 切换红绿灯
async exchange() {
if (!this.isStop) {
if (this.remain > 0) {
// 存在剩余时间不切换
// console.log(`当期信号灯【${this.sig}】,剩余时间【${Math.round(this.remain)}】`)
this.emit('tick')
// 我们希望每一秒钟提示一次
await delay(1 * 1000)
} else {
this.sig = this.next
this.setTime()
console.log(`切换信号灯【${this.sig}】`)
this.emit('change')
}
this.exchange()
}
}
// 停止
stop() {
console.log('停止循环=====>')
this.isStop = true
}
// 重启
reStart() {
console.log('重启=====>')
this.isStop = false
this.exchange()
}
}
const s = new Single({
init: 'Green',
times: [10, 3, 5]
})
const tickHandler = (e) => {
console.log('tickHandler=====>', e.sig, e.next, e.remain)
}
const changeHandler = (e) => {
console.log('changeHandler=====>', e.sig, e.next, e.remain)
}
s.on('change', changeHandler)
s.on('tick', tickHandler)
setTimeout(() => {
s.stop()
}, 15000);
setTimeout(() => {
s.reStart()
}, 20000);
效果为:
【注意】当在使用的时候,stop方法停止循环可以在界面退出,息屏,app退出时使用;进入界面时,可以使用reStart方法重启,进入时重新计算时差
setTimeout/setInterval、requestAnimationFrame 三者的区别:
- 引擎层面: setTimeout/setInterval 属于 JS引擎,requestAnimationFrame 属于 GUI引擎;JS引擎与GUI引擎是互斥的,也就是说 GUI 引擎在渲染时会阻塞 JS 引擎的计算
- 时间是否准确:requestAnimationFrame 刷新频率是固定且准确的,但 setTimeout/setInterval 是宏任务,根据事件轮询机制,其他任务会阻塞或延迟js任务的执行,会出现定时器不准的情况
- 当页面被隐藏或最小化时,setTimeout/setInterval 定时器仍会在后台执行动画任务,而使用 requestAnimationFrame 当页面处于未激活的状态下,屏幕刷新任务会被系统暂停
参考文档
完结撒花...