代码改变世界

【javascript基础】【setTimeout setInterval】 之 How JavaScript Timers Work [译]

2012-11-13 15:03  sniper007  阅读(319)  评论(0编辑  收藏  举报

At a fundamental level it's important to understand how JavaScript timers work. Often times they behave unintuitively because of the single thread which they are in. Let's start by examining the three functions to which we have access that can construct and manipulate timers.

  • var id = setTimeout(fn, delay); - Initiates a single timer which will call the specified function after the delay. The function returns a unique ID with which the timer can be canceled at a later time.
  • var id = setInterval(fn, delay); - Similar to setTimeout but continually calls the function (with a delay every time) until it is canceled.
  • clearInterval(id);, clearTimeout(id); - Accepts a timer ID (returned by either of the aforementioned functions) and stops the timer callback from occurring.

In order to understand how the timers work internally there's one important concept that needs to be explored: timer delay is not guaranteed. Since all JavaScript in a browser executes on a single thread asynchronous events (such as mouse clicks and timers) are only run when there's been an opening in the execution. This is best demonstrated with a diagram, like in the following:


(Click to view full size diagram)

There's a lot of information in this figure to digest but understanding it completely will give you a better realization of how asynchronous JavaScript execution works. This diagram is one dimensional: vertically we have the (wall clock) time, in milliseconds. The blue boxes represent portions of JavaScript being executed. For example the first block of JavaScript executes for approximately 18ms, the mouse click block for approximately 11ms, and so on.

Since JavaScript can only ever execute one piece of code at a time (due to its single-threaded nature) each of these blocks of code are "blocking" the progress of other asynchronous events. This means that when an asynchronous event occurs (like a mouse click, a timer firing, or an XMLHttpRequest completing) it gets queued up to be executed later (how this queueing actually occurs surely varies from browser-to-browser, so consider this to be a simplification).

To start with, within the first block of JavaScript, two timers are initiated: a 10ms setTimeout and a 10ms setInterval. Due to where and when the timer was started it actually fires before we actually complete the first block of code. Note, however, that it does not execute immediately (it is incapable of doing that, because of the threading). Instead that delayed function is queued in order to be executed at the next available moment.

Additionally, within this first JavaScript block we see a mouse click occur. The JavaScript callbacks associated with this asynchronous event (we never know when a user may perform an action, thus it's consider to be asynchronous) are unable to be executed immediately thus, like the initial timer, it is queued to be executed later.

After the initial block of JavaScript finishes executing the browser immediately asks the question: What is waiting to be executed? In this case both a mouse click handler and a timer callback are waiting. The browser then picks one (the mouse click callback) and executes it immediately. The timer will wait until the next possible time, in order to execute.

Note that while mouse click handler is executing the first interval callback executes. As with the timer its handler is queued for later execution. However, note that when the interval is fired again (when the timer handler is executing) this time that handler execution is dropped. If you were to queue up all interval callbacks when a large block of code is executing the result would be a bunch of intervals executing with no delay between them, upon completion. Instead browsers tend to simply wait until no more interval handlers are queued (for the interval in question) before queuing more.

We can, in fact, see that this is the case when a third interval callback fires while the interval, itself, is executing. This shows us an important fact: Intervals don't care about what is currently executing, they will queue indiscriminately, even if it means that the time between callbacks will be sacrificed.

Finally, after the second interval callback is finished executing, we can see that there's nothing left for the JavaScript engine to execute. This means that the browser now waits for a new asynchronous event to occur. We get this at the 50ms mark when the interval fires again. This time, however, there is nothing blocking its execution, so it fires immediately.

Let's take a look at an example to better illustrate the differences between setTimeout and setInterval.

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

These two pieces of code may appear to be functionally equivalent, at first glance, but they are not. Notably the setTimeout code will always have at least a 10ms delay after the previous callback execution (it may end up being more, but never less) whereas the setInterval will attempt to execute a callback every 10ms regardless of when the last callback was executed.

There's a lot that we've learned here, let's recap:

  • JavaScript engines only have a single thread, forcing asynchronous events to queue waiting for execution.
  • setTimeout and setInterval are fundamentally different in how they execute asynchronous code.
  • If a timer is blocked from immediately executing it will be delayed until the next possible point of execution (which will be longer than the desired delay).
  • Intervals may execute back-to-back with no delay if they take long enough to execute (longer than the specified delay).

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.

 

 

 

从一个基础的层面来讲, 理解JavaScript中的timers(定时器, 两种称呼在下文均有可能使用) 如何工作是十分重要的。 由于timers所处的单线程机制, 通常使他们的行为和我们直观想像的不同。 先来看一下以下三个可以让我们构造和操作timers的函数。

  • var id = setTimeout(fn, delay); - 初始化一个单独的timer, 用来在一定时间间隔之后调用特定的函数。 这个函数返回一个唯一的标识ID, 用来随后取消这个timer。
  • var id = setInterval(fn, delay); - 类似于setTimeout函数, 区别是每隔一个周期(时间间隔)都会调用同一个函数, 直到被取消。
  • clearInterval(id);, clearTimeout(id); - 接收一个标识ID(前面所提到的函数返回的), 停止timer回调函数的发生。

为了理解timers内部如何工作, 有一个重要的概念需要探明: timer的发生并不被保证。 由于所有JavaScript程序在浏览器中执行单线程异步事件(asynchronous events)(比如鼠标点击和timers)当且仅当执行过程中有执行机会时执行(即需要按顺序执行, 等待空闲)。 我们可以通过下图去表明这一观点:

这个图表里有很多信息值得去仔细体会, 一旦完全理解了, 你将对JavaScript如何异步执行工作有一个更好的认识。 这是一个一维的图表, 垂直的表示时间, 按毫秒计算。 蓝色的方块代表JavaScript正在被执行的部分。 比如一个JavaScript执行方块的执行时间大概是18毫秒, 鼠标点击(Mouse Click Callback)的执行方块大概是11毫秒, 等等。

由于JavaScript只能在同一时间执行一个段代码(由他单线的特性所决定) , 这些执行代码块中的每个都会“阻塞”其他的异步事件的进行。 这意味着当一个异步事件发生(比如鼠标点击, 定时器触发, 或异步请求)时, 他会排到执行队列后面(这个队列如何排列在各浏览器中的行为是不同的, 所以这样考虑是一个简化)。

从第一个JavaScript执行代码块开始分析, 有两个定时器被初始化: 一个10毫秒的setTimeout和一个10毫秒间隔的setInterval。 根据定时器何时何地被初始化, 他实际上会在第一个执行方块完成之前被触发。 注意, 触发后不会马上执行(由于单线程的原因) 。 而被延后的函数将排在执行队列之后, 并且等到下一个可能的执行时刻执行。

另外, 在第一个执行块中, 我们看到一个鼠标点击发生了。 一个JavaScript回调绑定在这个异步事件了(我们永远不会知道用户何时会执行这样一个操作, 因此他被考虑成异步的), 但不能立刻被执行, 因此, 像开始的定时器一样, 排在执行队列后面等待执行。

在第一个初始化的执行块结束后, 浏览器立刻询问: 谁是下一个等待被执行的。 这时, 鼠标点击处理函数和定时器回调函数都在等待执行。 于是浏览器选择了鼠标点击事件的回调函数并立即执行。 定时器将等到下一个可能的时间去执行。

注意, 在鼠标点击处理函数执行时, 第一次interval回调函数触发了。 和定时器一起, 他的处理函数排入队列等待随后的执行。 然而, 注意当这个interval第二次触发时(当setTimeout的处理函数正在执行时), 他的处理函数被抛弃掉了。 如果把所有的interval回调函数都排在执行队列后面,当一大段代码执行完之后, 结果便会是很多interval的处理函数一个接着一个没有间隙的执行, 直到全部完成。因此, 浏览器倾向于简单的当没有更多interval的处理函数排队时再将下一个处理函数排到队尾。

我们可以看到, 当第三个interval触发时, 之前的interval函数正在执行。 这也证实了一点, interval并不关注当前执行的是什么, 而是直接排到执行队列后面。 这就意味着两次interval回调函数之间的时间间隔有可能被牺牲(缩短)。

最后, 在第二个interval回调函数执行结束之后, 我们可以看到没有任何其他等待JavaScript引擎去执行的了。 这意味着浏览器现在等待这一个新的异步事件发生。 我们在50毫秒时等来了interval的又一次触发。 这时没有任何其他事件阻塞他了, 所以立即就执行了。

让我们通过一个实例来更好的证明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在执行异步代码时从根本上是不同的。
  • 如果一个定时器被阻塞而不能立即执行, 他会延迟到下一个可能的执行点(通常会比期望的时间间隔长)
  • 如果Interval的处理函数的执行时间足够长(比指定的间隔时间长), 他会背靠背的执行而没有间隔。

上述这些都是非常重要的需要了解的知识。 了解JavaScript引擎如何工作, 特别的当大量的异步事件发生时, 才能为构建优质的应用程序代码打好基础。

 

参考文档:

http://ejohn.org/blog/how-javascript-timers-work/ 

http://zamanewby.yo2.cn/?p=638698