javascript定时器工作原理

时间:2021-01-18 00:10:13

原文地址:http://ejohn.org/blog/how-javascript-timers-work/

在基础知识层面上,了解javascript定时器的工作原理是很重要的。由于javascript是工作在一个单线程环境中,所以它们经常表现出一些违反直觉的行为。下面我们就从创建和操作三个定时器入手来分析定时器的工作原理。

  • var id=setTimeout(fn,delay); 初始化一个定时器,它将在delay延迟后触发fn函数。setTimeout方法返回一个用来标示定时器的唯一id,生成唯一id之后可以用这个id来取消所标示的定时器;
  • var id=setInterval(fn,delay);和setTimeout类似,不过setInterval会持续的每隔delay延迟后调用fn方法,直到它被取消;
  • clearInterval(id);clearTimeout(id);这两个方法都接收一个定时器id(前面两个函数的返回值)作为参数,用来取消相应的定时器。

为了理解定时器内部是如何工作的,我们需要探索这样一个重要的概念:定时器的延迟是不能被保证的。因为在浏览器中javascript都是在一个单线程中执行的,异步事件(比如鼠标点击和定时器)只有在执行环境有空隙的时候才会执行。为了更好的理解,用如下的图示进行说明。

javascript定时器工作原理

上面的图示中有很多需要理解的信息,不过一旦完全理解了,你会对javascript异步执行有个清晰的认识。上面的图表是一维的,竖直方向是以毫秒为单位的时间,蓝色块代表javascript事件执行。比如,第一个javascript代码块大约执行了18ms,鼠标点击事件块大约执行了11ms等等。

因为javascript同一时间只能执行一块代码(这是因为它的单线程特点决定的),每一个块的执行都会阻塞其它的异步事件,这就意味着当一个异步事件产生(比如一次鼠标点击,定时器触发,或者一个XMLHttpRequest完成)会被加入到一个事件队列中等待被执行(不同的浏览器处理事件队列的方式有很大的不同,但大概的过程是如此)。

在上图示意中,在第一个javascript代码块执行中,初始化了两个定时器,分别为10ms的setTimeout和10ms的setInterval。当普通定时器触发的时候第一个javascript代码块还没执行完,所以回调函数会被加入到队列里等待被执行。

另外,在第一个javascript代码块里有一次鼠标点击产生了,鼠标点击的回调函数也是一个异步事件(因为我们不知道用户点击时间什么时候会发生,所以也认为它是一个异步事件)同样也不会立即被执行,它也被放入队列中等待被执行。

当第一个代码块执行完毕,浏览器会立即查询等待被执行的队列,在上图示意中,鼠标点击事件和定时器都在等待队列中,浏览器会按照产生的时间顺序选择鼠标点击事件来执行,定时器会继续等待直到下次空闲时间。

注意到,当鼠标点击事件回调函数执行时,第一个间隔定时器(interval)回调函数被触发了,同普通定时器一样它的回调函数被放入队列中等待执行。然而,注意到当间隔定时器再次被触发时(当普通定时器回调函数正在被执行时),这次的间隔定时器回调函数会被丢弃而不会被放入队列中,因为,如果在一个非常大的javascript代码块执行过程中把每次间隔定时器触发的回调函数都放入队列的话,导致的结果是当这个大javascript代码块执行完毕时,有很多的回调函数会被无延迟的依次执行(这和我们当初需要间隔执行的目的相违背),所以,浏览器采用的作法是当等待队列中没有间隔定时器的回调函数时才会放入一个间隔定时器的回调函数。

实际上我们可以看见,这正是导致当第三个间隔定时器被触发时有一个间隔定时器回调函数正在被执行。这给我展示了一个重要的事实,间隔定时器不关心当前是哪个事件正在执行,它会立即被放入到等待队列中,这就意味着这两个间隔定时器回调函数将会无间隔的执行。

最后,在第二个间隔定时器回调函数执行完毕后,我们可以看见已经没有事件等待被执行了,这就意味着浏览器等待一个新的异步事件发生。我们注意到在第50ms的时候,间隔定时器再次被触发了,在当前事件,等待队列中没有事件,所以这次的回调函数立即执行了。

让我们看一个能更加说明setTimeout和setInterval之间区别的例子。

setTimeout(function(){
     /* Some long block of code... */
     setTimeout(arguments.callee, 10);
   }, 10);
   
  setInterval(function(){
     /* Some long block of code... */
   }, 10);

第一眼看上去的时候你会觉得这两块代码的执行效果会是一样的,但是,它们是不同的。值得注意的是,setTimeout的执行时间永远是10ms(只可能多不可能少),而setInterval会尝试着每隔10ms执行一次,而会忽略上一次的执行时间。

通过上述我们学习到很多,让我们总结一下:

  • javascript引擎是单线程,它会强制异步事件排队等待执行;
  • setTimeout和setInterval执行原理是不一样的;
  • 如果一个普通定时器被阻塞了,它会等待直到有合适的执行时间(等待时间有可能比它定义的延迟时间长);
  • 如果间隔定时器回调函数执行时间很长(长于定义的间隔时间)的话,间隔定时器有可能无间隔的持续执行。

以上的这些知识都是非常重要的,了解javascript引擎是怎样工作的,尤其是当大量的异步事件发生时。了解这些是写出杰出代码的基础。