背景
客服系统需要检测坐席是否下线,否则服务端将进线分配给离线的坐席,消费者得不到坐席的回应,影响消费者体验。系统为了检测消费者是否下线,增加了心跳逻辑,页面每2秒上报一次心跳。服务端30秒内未收到心跳,就认为坐席下线。
现网偶发坐席不断切换状态(上线->下线->上线->下线…)
经定位现代浏览器为了省电,当页面切到后台(非当前活动页),5分钟后浏览器会降低js定时器的执行频率(1分钟一次)。导致心跳机制受到影响。
该设计具体可以google(Throttling Javascript Timers to Reduce Battery Usage in Background Tabs)
解决方案
经google,截止到目前webworker不受该影响,可以通过webworker模拟settimeout、setInterval。
现在已有基于webworker,模拟settimeout、setInterval来规避该问题的开源库HackTimer
基本原理就是通过webworker模拟settimeout、setInterval并替换掉原生的settimeout、setInterval,源码在后面章节进行分析。先看测试效果。
准备工作
通过vue-cli创建demo
测试一
在HelloWorld.vue中添加setInterval,然后打开控制台并将页面切到后台,5分钟后观察日志打印。
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
setInterval(() => {
console.log("log-"+new Date())
}, 25000);
</script>
结果
日志从8:52分开始打印,5分钟后日志打印变成1分钟一次。
测试二
安装HackTimer,并在入口(main.js)引入。
import 'hacktimer'
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
结果
日志从19:38分开始打印,5分钟后日志打印频率仍未下降,可见webworker目前仍然不受该限制。
hacktimer源码分析
webworker是为了解决js单线程性能受限引入的,其功能与线程相似,有些不与界面交互的逻辑放到webworker中运行,能够利用多核,提高页面性能。
hacktimer整理流程如下;简而言之,周期调度由WebWorker执行,WebWorker生成周期信号,驱动主线程执行handler:
hacktimer里面涉及几个技术点
- 通过window.URL.createObjectURL(blob)动态生成WebWorker的脚本
var blob = new Blob (["\
var fakeIdToId = {};\
onmessage = function (event) {\
var data = event.data,\
name = data.name,\
fakeId = data.fakeId,\
time;\
if(data.hasOwnProperty('time')) {\
time = data.time;\
}\
switch (name) {\
case 'setInterval':\
fakeIdToId[fakeId] = setInterval(function () {\
postMessage({fakeId: fakeId});\
}, time);\
break;\
case 'clearInterval':\
if (fakeIdToId.hasOwnProperty (fakeId)) {\
clearInterval(fakeIdToId[fakeId]);\
delete fakeIdToId[fakeId];\
}\
break;\
case 'setTimeout':\
fakeIdToId[fakeId] = setTimeout(function () {\
postMessage({fakeId: fakeId});\
if (fakeIdToId.hasOwnProperty (fakeId)) {\
delete fakeIdToId[fakeId];\
}\
}, time);\
break;\
case 'clearTimeout':\
if (fakeIdToId.hasOwnProperty (fakeId)) {\
clearTimeout(fakeIdToId[fakeId]);\
delete fakeIdToId[fakeId];\
}\
break;\
}\
}\
"]);
// Obtain a blob URL reference to our worker 'file'.
workerScript = window.URL.createObjectURL(blob);
- 拦截(模拟)settimeout、setInterval
window.setInterval = function (callback, time /* , parameters */) {
var fakeId = getFakeId ();
fakeIdToCallback[fakeId] = {
callback: callback,
parameters: Array.prototype.slice.call(arguments, 2)
};
worker.postMessage ({
name: 'setInterval',
fakeId: fakeId,
time: time
});
return fakeId;
};
window.clearInterval = function (fakeId) {
if (fakeIdToCallback.hasOwnProperty(fakeId)) {
delete fakeIdToCallback[fakeId];
worker.postMessage ({
name: 'clearInterval',
fakeId: fakeId
});
}
};
window.setTimeout = function (callback, time /* , parameters */) {
var fakeId = getFakeId ();
fakeIdToCallback[fakeId] = {
callback: callback,
parameters: Array.prototype.slice.call(arguments, 2),
isTimeout: true
};
worker.postMessage ({
name: 'setTimeout',
fakeId: fakeId,
time: time
});
return fakeId;
};
window.clearTimeout = function (fakeId) {
if (fakeIdToCallback.hasOwnProperty(fakeId)) {
delete fakeIdToCallback[fakeId];
worker.postMessage ({
name: 'clearTimeout',
fakeId: fakeId
});
}
};
- setInterval、settimeout的callback注册到hacktimer自己私有的变量中,并监听WebWorker发送的调度信号,执行注册的方法。
worker.onmessage = function (event) {
var data = event.data,
fakeId = data.fakeId,
request,
parameters,
callback;
if (fakeIdToCallback.hasOwnProperty(fakeId)) {
request = fakeIdToCallback[fakeId];
callback = request.callback;
parameters = request.parameters;
if (request.hasOwnProperty ('isTimeout') && request.isTimeout) {
delete fakeIdToCallback[fakeId];
}
}
if (typeof (callback) === 'function') {
callback.apply (window, parameters);
}
};