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 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.

告别alert——善用Firebug的日志功能

作为一个Javascript程序员,可能遇到的最头痛的问题就是程序的调试了。一方面是因为Javascript绝大多数情况下都是作为浏览器的脚本来使用,而大多数的浏览器没有内置良好的调试功能;另外一个方面就是如今Javascript程序越写越庞大,如果没有一个称心如意的调试工具,用ExtJS这样庞大的JS框架来做开发简直就是自寻烦恼。

以前Javascript只是用来做一些页面的特效或者是表单验证的时候,程序规模很小,调试起来也就没那么多讲究,一个简单的alert函数差不多就足够了。随着Javascript越来越被重视,相应的框架也雨后春笋般的涌现出来。这时候如果你还是用alert来调试你的程序,那将是一件很痛苦的事情。

工具的力量是伟大的,今天就给大家介绍一款强大的Firefox插件——Firebug。作为Mozilla五星级推荐的插件,它的功能可真不是盖的(貌似Firebug的作者就是Mozilla浏览器DOM引擎的开发者)。今天我仅仅介绍一下Firebug中的Logging的功能,虽然简单,但熟悉之后再调试Javascript程序就会变得轻松很多。

Firebug提供了一个console对象,在插件加载的时候就注册到Javascript的运行环境中去了,可以在你的程序中直接使用。

console对象提供了一个log方法,在你的程序中调用这个方法,就可以把信息输出到Firebug的控制台面板中:

console.log('Hello, World!);

firebug-console-log

一次也可以输出多个变量:console.log('hello', name, ...);

同时,console.log 还支持格式化字符串的输出,你可以用类似C语言中printf的语法来调用这个函数:

console.log("%s is %d years old.", "Bob", 42)

更强大的是如果你给log函数传的是一个对象或者数组、HTML元素等,它会在控制台中显示为一个超链接,点击就可以查看该元素的详细信息。

单单是一个log函数就可以实现程序中数据方便的查看,另外Firebug还提供了另外一些方便的函数实现日志的彩色输出:

console.info()
console.warn()
console.debug()
console.error()

screenlogging-colors

怎么样,漂亮吧,以后调试Javascript程序也会变成一件快乐的事情吧 :-)

这仅仅是Firebug功能中的冰山一角,Firebug的功能可不止如此,有时间多研究研究吧,相信你会喜欢上这个插件的。