一个 web socket 链接,为了保持活跃,需要定时向服务器发送心跳信令。该机制通常由 setInterval 实现。
但是,当浏览器 Tab 页处于非激活状态下(浏览器最小化或切向其它 Tab 页)时,基于节能考虑,定时器会处于“休眠”或“降频”状态,在这种情况下,心跳机制就不正常了。
甚至在旧版 windows 系统中,可以看到关于 js 定时器刷新频率的设置:
Tab 页激活状态下,setInterval 最大频率可以达到200多的 fps(每秒触发次数);非激活状态下,只能达到 60 fps。
注:setInterval的回调执行间隔并不是由其第二个参数 delay 决定的。即使在激活状态下,也受限于当前 js 主线程的执行队列是否拥挤。
那么,如何解决这个问题呢?如何使 Tab 页在非激活状态下,尽量保持相对准确的触发呢?
解决方法,可以引用这个 js 库:
https://github.com/myonov/momentum
其它代码不变,正常使用 setInterval、setTimeout等定时器方法。
翻看momentum的源码,实现原理很简单。主要是通过创建一个看不见的独立线程,在后台运行定时器,并接管默认的定时器方法实现。
var containerFunction = function () {
var idMap = {};
self.onmessage = function (e) {
if (e.data.type === 'setInterval') {
idMap[e.data.id] = setInterval(function () {
self.postMessage({
type: 'fire',
id: e.data.id
});
}, e.data.delay);
} else if (e.data.type === 'clearInterval') {
clearInterval(idMap[e.data.id]);
delete idMap[e.data.id];
} else if (e.data.type === 'setTimeout') {
idMap[e.data.id] = setTimeout(function () {
self.postMessage({
type: 'fire',
id: e.data.id
});
// remove reference to this timeout after is finished
delete idMap[e.data.id];
}, e.data.delay);
} else if (e.data.type === 'clearCallback') {
clearTimeout(idMap[e.data.id]);
delete idMap[e.data.id];
}
};
};
在该容器中,支持 setInterval、clearInterval、setTimeout、clearCallback 四个方法。
return new Worker(URL.createObjectURL(new Blob([
'(',
containerFunction.toString(),
')();'
], {type: 'application/javascript'})));
new Blob
构造函数是基于代码字符串,创建一个不可变、原始数据的类文件对象。URL.createObjectURL()
静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的 URL。new Worker 接受一个 js 脚本的 url,启动一个后台线程。这个后台线程不受浏览 Tab 激活/关闭的影响。
window对象上默认的全局方法,均可以重写:
window.setInterval = patchedSetInterval;
window.clearInterval = patchedClearInterval;
window.setTimeout = patchedSetTimeout;
window.clearTimeout = patchedClearTimeout;
通过以上方法,项目中其它地方调用 setInterval,真正执行的均是自定义的 patchedSetInterval。以patchedSetInterval、patchedClearInterval为例,具体实现是这样的:
function patchedSetInterval(callback, delay) {
var intervalId = generateId();
$momentum.idToCallback[intervalId] = callback;
$momentum.worker.postMessage({
type: 'setInterval',
delay: delay,
id: intervalId
});
return intervalId;
}
function patchedClearInterval(intervalId) {
$momentum.worker.postMessage({
type: 'clearInterval',
id: intervalId
});
delete $momentum.idToCallback[intervalId];
}
在注册定时器时,通过 postMessage,向 worker 发送一条消息。独立线程容器的 onmessage内部,监听这条消息:
if (e.data.type === 'setInterval') {
idMap[e.data.id] = setInterval(function () {
self.postMessage({
type: 'fire',
id: e.data.id
});
}, e.data.delay);
}
worker 内部的代码是完全独立的,其内对 setInterval 的调用,是对原生定时器方法的调用,与主线程的接管方法 patchedSetInterval 无关。worker 与 js 主线程只能通过 postMessage、onmessage这样的方式通讯。
在 momentum 主线程代码内,以下代码是对 worker 线程消息的监听:
$momentum.worker.onmessage = function (e) {
if (e.data.type === 'fire') {
$momentum.idToCallback[e.data.id]();
}
};
idToCallback 是外部缓存的回调函数集合,当接收到 fire 指令时,触发对应的回调函数。
setImmediate
并不支持。