JS的定时器目前有三个:setTimeout、setInterval和setImmediate。
定时器也是一种异步任务,通常浏览器都有一个独立的定时器模块,定时器的延迟时间就由定时器模块来管理,当某个定时器到了可执行状态,就会被加入主线程队列。
JS定时器非常实用,做动画的肯定都用到过,也是最常用的异步模型之一。
有时候一些奇奇怪怪的问题,加一个setTimeout(fn, 0)(以下简写setTimeout(0))就解决了。不过,如果对定时器本身不熟悉,也会产生一些奇奇怪怪的问题。
setTimeout
setTimeout(fn, x)表示延迟x毫秒之后执行fn。
使用的时候千万不要太相信预期,延迟的时间严格来说总是大于x毫秒的,至于大多少就要看当时JS的执行情况了。
另外,多个定时器如不及时清除(clearTimeout),会存在干扰,使延迟时间更加捉摸不透。所以,不管定时器有没有执行完,及时清除已经不需要的定时器是个好习惯。
HTML5规范规定最小延迟时间不能小于4ms,即x如果小于4,会被当做4来处理。 不过不同浏览器的实现不一样,比如,Chrome可以设置1ms,IE11/Edge是4ms。
setTimeout注册的函数fn会交给浏览器的定时器模块来管理,延迟时间到了就将fn加入主进程执行队列,如果队列前面还有没有执行完的代码,则又需要花一点时间等待才能执行到fn,所以实际的延迟时间会比设置的长。如在fn之前正好有一个超级大循环,那延迟时间就不是一丁点了。
(function testSetTimeout() {
const label = 'setTimeout';
console.time(label);
setTimeout(() => {
console.timeEnd(label);
}, 10);
for(let i = 0; i < 100000000; i++) {}
})();
结果是:setTimeout: 335.187ms,远远不止10ms。
setInterval
setInterval的实现机制跟setTimeout类似,只不过setInterval是重复执行的。
对于setInterval(fn, 100)容易产生一个误区:并不是上一次fn执行完了之后再过100ms才开始执行下一次fn。 事实上,setInterval并不管上一次fn的执行结果,而是每隔100ms就将fn放入主线程队列,而两次fn之间具体间隔多久就不一定了,跟setTimeout实际延迟时间类似,和JS执行情况有关。
(function testSetInterval() {
let i = 0;
const start = Date.now();
const timer = setInterval(() => {
i += 1;
i === 5 && clearInterval(timer);
console.log(`第${i}次开始`, Date.now() - start);
for(let i = 0; i < 100000000; i++) {}
console.log(`第${i}次结束`, Date.now() - start);
}, 100);
})();
输出
1
2
3
4
5
6
7
8
9
10
第1次开始 100
第1次结束
1089
第2次开始 1091
第2次结束
1396
第3次开始 1396
第3次结束
1701
第4次开始 1701
第4次结束
2004
第5次开始 2004
第5次结束
2307
可见,虽然每次fn执行时间都很长,但下一次并不是等上一次执行完了再过100ms才开始执行的,实际上早就已经等在队列里了。
另外可以看出,当setInterval的回调函数执行时间超过了延迟时间,已经完全看不出有时间间隔了。
如果setTimeout和setInterval都在延迟100ms之后执行,那么谁先注册谁就先执行回调函数。
setImmediate
这算一个比较新的定时器,目前IE11/Edge支持、Nodejs支持,Chrome不支持,其他浏览器未测试。
从API名字来看很容易联想到setTimeout(0),不过setImmediate应该算是setTimeout(0)的替代版。
在IE11/Edge中,setImmediate延迟可以在1ms以内,而setTimeout有最低4ms的延迟,所以setImmediate比setTimeout(0)更早执行回调函数。不过在Nodejs中,两者谁先执行都有可能,原因是Nodejs的事件循环和浏览器的略有差异。
(function testSetImmediate() {
const label = 'setImmediate';复制代码
console.time(label);复制代码
setImmediate(() => {
复制代码
console.timeEnd(label);
});
})();
Edge输出:setImmediate: 0.555 毫秒
很明显,setImmediate设计来是为保证让代码在下一次事件循环执行,以前setTimeout(0)这种不可靠的方式可以丢掉了。
总结:
以上内容都是查阅资料得来,我们目前只了解一个setInterval计时器。有兴趣的可以了解一下。