浅谈JavaScript中的定时器

时间:2021-06-03 06:44:23

引言

  使用setTimeout()和setInterval()创建的定时器可以实现很多有意思的功能。很多人认为定时器是一个单独的线程(之前我也是),但是JavaScript是运行在单线程环境中的,而定时器只是计划代码在未来的某个时间执行。执行时间是不能保证的,在页面的生命周期中,会不断有其他的代码在控制着JavaScript的执行主线程。比如:在页面下载完成以后的JavaScript代码需要执行、事件处理函数、Ajax的回调函数都需要通过主线程来执行。浏览器只负责排序,指定某一段代码在某一时刻需要被执行。

  下面我们通过一个例子来详细的了解下。

  我们可以把JavaScript想象成在时间线上执行的。例如:页面刚加载时,首先执行的是包含在<script>元素中的代码,这通常是一些简单的函数声明和变量的初始化。在这时候,JavaScript主线程就会等待更多代码执行。当线程空闲的时候,下一段代码会被触发并立即执行。这样一个页面的时间线类似于下图:

浅谈JavaScript中的定时器

  在这张图片中,我们可以生动的看到JavaScript主线程的执行机制。其实,在JavaScript主线程之外,还有一个保存下一个即将被执行的代码的队列存在。随着页面在其生命周期中推移,代码会按照执行顺序放入队列,并在未来的某一个时间执行代码。例如:当接收到某一个Ajax响应时,其回调函数会被放入队列中。在JavaScript中是没有任务代码能立即执行的,但是一旦主线程空闲,队列中的代码会被立即执行。

  定时器对于队列的工作方式是这样的:当特定时间过去后,将定时器代码插入到队列中。注意:给队列添加代码不会意味着立即执行,只是表示会尽快执行。例如:我们在定时器中设定延时200毫秒。并不是意味着200毫秒以后需代码一定会执行。只是200毫秒以后这段代码会被添加到执行队列中。如果此时执行队列为空,那么代码会被立即执行。其他情况下代码可能需要等待更长的时间来执行。

  请看下面的代码:

 var btn = document.getElementById("btn");
btn.onclick = function () {
setTimeout(function () {
alert("Hello");
}, 250);
//其他代码.....
}

  在代码中,我们给按钮添加了事件处理程序。事件处理程序设置了一个250毫秒以后执行的定时器。点击按钮后,首先进入执行队列的是onclick事件处理函数。再过250毫秒以后,定时器的代码才会被添加到执行队列中。如果我们假设前面的onclick事件处理函数需要执行300毫秒,那么定时器代码至少需要300毫秒一以后才会执行(因为300毫秒以后JavaScript主线程执行完事件处理函数后,空闲状态,可以立即执行定时器的代码)。下面我们画一张时序图来形象的描述下这个过程。如图:

  浅谈JavaScript中的定时器

  在图片中我看到,在255毫秒的时候定时器代码被添加到执行队列了。但是此时还无法运行。因为主线程在执行事件处理函数。当主线程空闲后,会立即执行定时器代码。

  重复的定时器

  使用setInterval定时器可以保证定时器代码规则的插入队列中。但是这是有问题的。比如:定时器代码可能在代码再次被添加到队列之前还没有执行完毕,结果导致定时器代码连续运行好几次,之间没有任何停顿。不过现代的JavaScript引擎可以避免这种问题。不过这种重复定时器的规则还是有两个弊端。

  1、某些间隔会被跳过。

  2、多个定时器的代码执行之间的间隔可能比预期的小。

  假设某一个事件处理程序使用setInterval设置了一个200毫秒的重复定时器。如果事件处理程序需要300多毫秒才执行完毕,同时定时器代码页花了差不多时间,那么就会跳过一个定时器间隔,因为前一个定时器的代码还没有执行完毕。请看下图:

浅谈JavaScript中的定时器

  我们看这张图,我们看到在5毫秒的时候我们创建了重复的定时器,在205毫秒的时候,队列中添加了第一个定时器代码。但是事件处理程序需要300多毫秒才会执行完毕。事件处理函数执行完毕以后,主线程立即执行队列中的定时器代码。在400多毫秒的时候,第二个定时器代码被添加到队列中。但是单到了600多毫秒时,第三个定时器代码会被跳过,因为当前执行队列存在未执行的代码(第二个定时器代码)。

  解决方案

 setTimeout(function(){
//处理代码
setTimeout(argument.callee,interval);
},interval);

  在这段代码中,我们使用了链式调用setTimeout()模式。每一次函数调用都会重新创建一个新的定时器。第二个setTimeout函数使用了argument.callee来获取当前执行函数的引用,并且设置了另一个新的定时器。这样做的好处是:在前一个定时器代码执行之前,不会往队列中添加新的定时器代码,不会存在任何间隔。同时也可以保证在下一次定时器代码执行之前,至少需要等待一段时间(interval的值),避免了连续执行。