原文:http://blog.anbutu.com/javascript/how-javascript-timers-work
对于一个编写基础代码的程序员来说,理解Javascript定时器的工作原理是很重要的。由于Javascript的定时器工作在一个单线程的环
境中,因此它们常常表现出一些违反直觉的行为。下面我们就首先从三个被用来创建和操作定时器函数入手来分析定时器的工作原理。
- var id = setTimeout(fn, delay); 该函数初始化一个延迟为 delay
的定时器并返回该定时器的id,在定时器触发前,我们可以通过返回的定时器id来取消这个定时器。当该定时器触发时将调用 fn 这个函数。
- var id = setInterval(fn, delay); 该函数和 setTimeout 类似,但它会每隔 delay
的时间间隔调用 fn 函数直到该定时器被取消。
- clearInterval(id);, clearTimeout(id);
这两个函数都接受一个定时器id(前面两个函数的返回值)作为参数,用来取消相应的定时器。
为了搞清楚定时器的内部是如何工作的,我们需要证实这样一个事实:定时器的延迟时间是不能够被保证的。这是因为所有在浏览器环境中的
Javascript 都是在一个
单线程中执行的,只有在遇到两个“执行窗口”的缝隙的时候那些异步的事件(用户点击鼠标、定时器触发)才能被执行。下面这个图例很好的演示了这一点:
javascript timers
上面的图中包含了很多需要理解的信息,一旦完全理解了这些,你会对 Javascript
的异步时间的执行有更加清晰的认识。在上面的这个一维的图示中,竖直方向是以微妙 为单位的时间,蓝色框代表 Javascript
执行的代码块。例如,上图中第一个代码块执行时间约为18毫秒,鼠标点击(Mouse Click)事件的回调函数执行了大约11毫秒。
因为 Javascript
在同一时间只能执行某个代码块(这是由它单线程的本质决定的),当这个代码块执行的时候,异步事件的响应就被“阻塞”了,这意味着这时候产生
的异步事件(鼠标点击、定时器触发、XMLHttpRequest请求完成)被加入到一个队列中(不同的浏览器处理事件缓存的方式有很大的差异,这里我们
只需要认为事件被放入了 一个队列就可以了)等待着下次机会执行。
在上图中,在第一个代码块执行期间初始化了两个定时器:一个10毫秒的 setTimeout 和一个10毫秒的
setInterval。由于在定时器触发的时候,第一个代码块还没有执行完成,
因此定时器设定的回调函数不会被立即执行,相反它会被加入队列等待下次机会执行。
另外,在第一个代码块执行过程中发生了一次鼠标点击事件,与这个异步事件(因为我们不能确定鼠标点击事件什么时候会发生,因此也认为它是异步的)相
关联的回调函数也 不会立即执行,它同样被加入队列等待下次机会执行。
当第一个代码块执行完,浏览器会查询是否有等待执行的任务?而当前情况下,鼠标点击事件和定时器的回调函数都等待执行,于是浏览器按照顺序先取出鼠
标点击事件的回调函数并立即执行它,而定时器的回调函数仍需要等待下次机会执行。
我们注意到,在鼠标点击事件的回调函数执行过程中,“间隔定时器”(interval)被触发,和普通定时器一样,它的回调函数也被加入队列等待机
会执行。但是,当“间隔定时器”再次被触发(在普通定时器的回调函数的执行期间)的时候,它的回调函数被丢弃而不是被加入等待队列。假设所有的“间隔定时
器”的回调函数无论如何都被加入等待队列的话,那么在执行一个非常大的代码块的时候就会有大批的回调函数被加入等待队列,等到代码块执行结束,这一批回调
函数就会无间隔的执行。与此相反,浏览器更倾向于在把“间隔定时器”的回调函数加入等待队列之前简单的等待直到等待队列中没有其他的“间隔定时器”的回调
函数。
实际上我们可以看到,这正是“间隔定时器”的回调函数正在执行的时候另一个个“间隔定时器”被触发的情形。这向我们揭示了一个重要的事实:“间隔定
时器”不关心当前正在运行的回调函数是什么,只是简单的把回调函数加入等待队列即使这意味着这两个回调函数将会无间隔的被执行。
最后,当第二个“间隔定时器”的回调函数执行结束,我们看到已经没有等待处理的回调函数了,这时候浏览器等待新的异步事件发生。当到达 50ms
标记的时候,“间隔定时器”再次被触发,这时候已经没有等待执行的任务,因此回调函数立即被执行。
下面让我们看一个更加能够说明 setTimeout 和 setInterval 之间区别的例子:
setTimeout(function(){
/* Some long block of code... */
setTimeout(arguments.callee, 10);
}, 10);
setInterval(function(){
/* Some long block of code... */
}, 10);
当第一眼看上去的时候也许你会觉得这两个函数功能是完全一样的,但实际上它们并不相同。使用 setTimeout
的函数会保证它们执行的间隔至少为 10 毫秒(只可能多不可能少),而使用 setInterval 的代码会尝试每隔 10
毫秒执行一次,它不会在乎上一次执行到现在的间隔有多少。
在上面我们学到了很多东西,让我们总结一下:
- Javascript 引擎是单线程的,致使异步事件的回调函数会被加入队列等待机会执行。
- setTimeout 和 setInterval 在如何执行异步代码上有根本的区别
- 如果定时器(的回调函数)不能够被立即执行,那么它将被推迟到下次机会执行(这将大于它预期的延迟)
- 如果回调函数执行时间过长(长于定时器的延迟时间),“间隔定时器”有可能会一个接一个无间隔的执行
All of this is incredibly important knowledge to build off of.
Knowing how a JavaScript engine works, especially with the large number
of asynchronous events that typically occur, makes for a great
foundation when building an advanced piece of application code.