理解JavaScript定时器:setTimeout和setInterval

定时器其实并不是JavaScript提供的,而是由浏览器(对于前端来说)提供的。所以setTimeout()setInterval()这两个方法均是通过浏览器的顶层对象window进行调用,可能平时大家在使用的过程中也会省去window而直接使用这两个方法。

这两个方法所接收的参数都一样:

1
2
setTimeout(func|code, delay);
setInterval(func|code, delay);

这两个方法总是被简单的认为:在多少毫秒之后就执行里面的函数或者每间隔多少毫秒就执行里面的函数,基于这种理解的话会遇到很多匪夷所思的坑。而结合上篇文章中所提到的执行队列来解释的话,很多疑问都可以迎刃而解。

前者:在指定的毫秒数后,将定时任务处理函数(func|code)添加到执行队列的队尾。

后者:按照指定的周期(以毫秒计),将定时任务处理函数(func|code)添加到执行队列的队尾。

下面分别使用了setIntervalsetTimeout来实现同一个功能,可运行查看效果。

这是相应的源代码:传送门

接下来继续填setInterval的坑。

假设定时器的上一个回调执行完到下一个回调开始的这段时间为时间间隔,那么对于setTimeout来说,这个时间间隔理论上是应该>=delay;而对于setInterval来说,这个时间间隔理论上是应该<=delay的。

但事实总会有出人意料的地方,setInterval就是那个制造意外的东西。

以下是常规的代码:

1
2
3
4
5
6
7
8
var endTime = null;
setInterval(count, 200);
function count() {
var elapsedTime = endTime ? (new Date() - endTime) : 200;
i++;
console.log('current count: ' + i + '.' + 'elapsed time: ' + elapsedTime + 'ms');
endTime = new Date();
}

其执行结果也比较符合理论时间,见下图。

接下来修改代码,让count()方法的执行时间变长一点:

1
2
3
4
5
6
7
function count() {
var elapsedTime = endTime ? (new Date() - endTime) : 200;
i++;
console.log('current count: ' + i + '.' + 'elapsed time: ' + elapsedTime + 'ms');
sleep(100); //sleep 100ms
endTime = new Date();
}

执行结果如下:

结合执行队列,可以用下图对上面两种情况进行直观的解释:

接下来再次修改代码,让count()方法的执行时间更长,设定为setIntervaldelay2倍,即400ms

1
2
3
4
5
6
7
function count() {
var elapsedTime = endTime ? (new Date() - endTime) : 200;
i++;
console.log('current count: ' + i + '.' + 'elapsed time: ' + elapsedTime + 'ms');
sleep(400); //sleep 400ms
endTime = new Date();
}

其执行效果变为如下:

意外发生了,每个回调之间的时间间隔竟然没有了,或者说缩短到非常小的间隔。事情大概是这样的:如果setInterval的定时时间到了,而前一个回调还没有执行完时,就会把这次的回调放在执行队列的队尾;如果setInterval的定时时间已经多次触发,而此时最前一个回调仍然还在执行,那么就会丢弃掉本次的回调。还是用图来直观说明吧。

这是回调处理时间比定时时间稍微长一点点的情况:

这是回调处理时间比定时时间长很多的情况:

所以,如果使用setInterval的话,其时间间隔总是让人捉摸不定。而使用setTimeout嵌套,则完全可以解决这个问题,还我们一个固定的时间间隔。