业务场景
在线状态管理
业务描述
前端:处于 在线,忙碌 状态的时候需要定时调用后台的心跳接口,和后台设定的说10秒一次的间隔,这个接口定义为工作人员是否在线的心跳。这个接口后台返回了 计费,使用权限,以及当前是否在线标识。因不可抗力原因未能使用上socket。
后台:每隔一分钟扫描一次心跳表,获取当前工作人员的在线状态,得知在线状态来做对应的在线分配接入等业务。
代码实现
实现方式上,考虑过:setTimeout,setInterval,requestAnimationFrame,咱货比三家
setTimeout:去消息队列排队的时候只需排一次队。
setTimeout(()=>{
console.log(1)
}, 500)
setTimeout(()=>{
console.log(2)
}, 1000)
setTimeout(()=>{
console.log(3)
}, 1500)
alert(4)
setInterval:需要按照预设的间隔时间,每到时间点都去排一下;setInterval去排队时,如果发现自己还在队列中未执行,则不会再去队列排队。也就是说,同一个inerval,在队列里只会有一个。
当然,这个地方执行三次 setInterval 也会有三次执行,不过就有了三个定时器了。
setInterval(()=>{
console.log('执行')
}, 500)
alert(1)
requestAnimationFrame:可以通过模拟实现 setTimeout,setInterval
最终的代码实现通过还是通过 setTimeout 来实现的。
let time
heartbeatTimer () {
time = setTimeout(() => {
clearTimeout(time)
// 开启下一次心跳
heartbeatTimer()
// 心跳接口请求
heartbeatTimerApi()
}, 10000)
}
出现的问题
通过 setTimeout 后发自行测试的时候是实现了间隔10秒一次的心跳接口请求,但是测试人员发现,在线状态会在自己未做状态更改的时候被后台返回的 online 值给离线了。
解决问题
起初是从 Event Loop 角度出发,觉得可能是主应用可能是执行比较吃性能的任务,或者一直在页面上进行操作,导致事件循环中一直有同步代码压入调用栈,从而一直无法让消息队列的任务进入到调用栈执行。
后续发现页面未有任何操作的情况下也被后端下线了, 原因还是因为心跳保持59秒都没有发起请求的情况导致了后台认为当前工作人员以离线。
和测试的沟通描述到一个场景就是出于在线的工作人员状态的时候, 有时候会将浏览器先收起来最小化,或者切换到别的页签的场景。
配合浏览器任务管理器 + 描述的场景 = 结论
设定了定时器的页面被后台的时候(浏览器最小化, 页签不被激活),随着浏览器的本身的线程调度策略降低,导致页面上的定时器执行情况也被降低了,导致心跳接口直接变成近分钟才会执行一次,后台刚好50秒间隔扫描一次,_(:з」∠)_ 这个运气也是没谁了。
let time = Date.now()
setInterval(() => {
console.log('距离上一次执行相差', Date.now() - time)
time = Date.now()
}, 500)
let time = Date.now()
function test() {
timer = setTimeout(() => {
test()
const tempTime = Date.now()
console.log('距离上一次执行的时间差:', tempTime - time)
time = tempTime
}, 500)
}
test()
诶, 要是我们通过 requestAnimationFrame 模拟 setTimeout 和 setInterval 呢 ?_(:з」∠)_
class RAF {
constructor () {
this.init()
}
init () {
this._timerIdMap = {
timeout: {},
interval: {}
}
}
run (type = 'interval', cb, interval = 16.7) {
const now = Date.now
let stime = now()
let etime = stime
//创建Symbol类型作为key值,保证返回值的唯一性,用于清除定时器使用
const timerSymbol = Symbol()
const loop = () => {
this.setIdMap(timerSymbol, type, loop)
etime = now()
if (etime - stime >= interval) {
if (type === 'interval') {
stime = now()
etime = stime
}
cb()
type === 'timeout' && this.clearTimeout(timerSymbol)
}
}
this.setIdMap(timerSymbol, type, loop)
return timerSymbol // 返回Symbol保证每次调用setTimeout/setInterval返回值的唯一性
}
setIdMap (timerSymbol, type, loop) {
const id = requestAnimationFrame(loop)
this._timerIdMap[type][timerSymbol]= id
}
setTimeout (cb, interval) { // 实现setTimeout 功能
return this.run('timeout', cb, interval)
}
clearTimeout (timer) {
cancelAnimationFrame(this._timerIdMap.timeout[timer])
}
setInterval (cb, interval) { // 实现setInterval功能
return this.run('interval', cb, interval)
}
clearInterval (timer) {
cancelAnimationFrame(this._timerIdMap.interval[timer])
}
}
var raf = new RAF()
var timer1 = raf.setInterval(() =>{
console.log(1000)
}, 1000)
var timer2 = raf.setInterval(() =>{
console.log(1500)
}, 1500)
raf.setTimeout(() => {
raf.clearInterval(timer1)
raf.clearInterval(timer2)
}, 6000)
引用的原址: https://juejin.cn/post/6999444668089892901#heading-11
然后出现了更离谱的事,通过模拟的定时器,在页面切后台等操作的时候直接不会执行 233,
为什么? 因为切换到其他页面的时候,requestAnimationFrame 获取不到浏览器更新的频率,回调函数也就不执行了。
解决办法
通过 web Worker 来新开辟一条线程来执行定时器任务,这个时候选择 setTimeout,setInterval 就都是可以的了
不过 webWorker 必须与其创建者同源
之前是尝试过 worker.js 文件放置于项目的public文件夹打包的时候以静态方式来去读取,不过在线上环境,public文件下的文件都被上传到oss服务器去了,存在跨域,与非同源了都一个问题。放目录下又被webpack给编译掉了文件名称。
为了解决此问题,通过 Blob 将代码段生成临时的JS文件来进行访问,就避免了上述的问题。
function createWorker (f) {
var blob = new Blob(['(' + f.toString() + ')()'])
var url = window.URL.createObjectURL(blob)
var worker = new Worker(url)
return worker
}
createWorker(()=>{
let time = Date.now()
setInterval(() => {
const tempTime = Date.now()
console.log('差值:', tempTime - time)
time = tempTime
}, 500)
})
因此,以后大家在使用到前端需要轮训到定时任务到时候,推荐使用 worker 来实现,不用担心 Event Loop 的队列执行问题,也不用担心浏览器线程调度的问题。
小知识 _(:з」∠)_
教大家怎么在控制台辨别当前网页是否开启了子线程
如果启动的子线程有请求的话,在 Network 面板下 请求也会携带一个线程图标在前面