wlbf.github.io

JavaScript Timer

最近遇到了setTimeout这个东西。发现了一篇著名的文章。网上已经有许多翻译,但我还是决定自己尝试翻译一下。如有错漏,欢迎指正。 原文: How JavaScript Timers Work

由于JavaScript的单线程特性,许多时候定时器的行为并不符合人们的预期。首先我们从三个函数开始,通过这三个函数我们可以构造和操纵定时器。

var id = setTimeout(fn, delay); 初始化一个简单的定时器。该定时器在一定延迟delay后,调用函数fn。并返回一个独特的ID,通过这个ID我们可以在以后某个时间注销该定时器。 var id = setInterval(fn, delay); 和setTimeout类似,但是会持续调用fn(间隔为delay)直到该定时器被注销 clearInterval(id);, clearTimeout(id); 接收一个(由上文涉及的两个函数返回)ID,且终止定时器回调函数的的调用。 为了理解定时器的工作原理,需要明确一个重要的概念。从定时器被设置到回调函数执行,中间的时间间隔并不一定等于我们设置的delay。因为在浏览器中所有单线程异步事件(比如鼠标点击和计时器),只有在线程空闲时才能执行。如下图所示: js-timer 这张图是一维的,垂直方向是时间轴,以毫秒计。蓝色方块代表被执行的JavaScript代码片段。例如第一段代码执行了18ms,鼠标点击事件执行11ms,以此类推。 由于JavaScript线程一次只能执行一段代码(由单线程特性决定),所以上图中的每一个代码块都阻塞了其他异步事件的执行。这意味着当异步事件发生时(例如鼠标点击、计时器触发、或者XMLHttpRequest完成)这些事件将被加入队列等待稍后执行(不同的浏览器组织这些队列的方式各不相同,本文所使用的是一个简化的模型)。 一开始,在第一段代码执行过程中,两个定时器被设置:一个间隔为10ms的setTimeout(即图中Timer starts)和一个间隔为10ms的setInterval。然而从setTimeout设置算起10ms之后setTimeout事件触发,此时第一段代码还没有执行完。所以setTimeout的回调函数不能立即得到执行(由于单线程而无法执行),于是将这个回调函数加入队列,等待以后线程空闲时执行。 此外,在第一段代码执行过程中发生了一个鼠标点击事件。与这个异步事件(我们无法预测用户什么时候点击鼠标,因此这个事件被视为异步的)相关的回调函数并不会立即执行。和计时器一样的,它被加入到队列的末尾,等待以后执行。 等到第一段代码执行完毕之后,浏览器立刻询问:接下来执行什么?此时处理鼠标点击事件的程序和计时器回调函数都处于等待执行的状态。浏览器从中选择一个(鼠标点击回调)并且立即执行它。想要执行计时器回调,必须等到下一次线程空闲。 接下来我们注意到当处理鼠标点击事件的程序正在执行时,第一个Interval事件触发。如上文所述这个事件的处理程序被加入队列等待以后执行。但是当Timer(即之前setTimeout)回调函数正在执行时,Inerval事件再次被触发,这次事件的处理程序没有被加入队列等待执行,而是被丢弃了。如果不这么做,在执行一很长一段代码的时候,你将所有的Interval回调函数都加入队列末尾,结果就是一大串Interval回调函数将会没有间隔地顺序执行,直到完成。相反,浏览器会等队列中没有别的Interval回调函数时,才会继续向队列中添加Interval回调函数。 事实上,我们可以看到,当Interval回调函数正在执行的时候,Interval第三次被触发。这反映出一个很重要的信息:Interval并不关心当前谁在执行,它的回调函数会不断地进入队列,即使两次回调函数的执行间隔收到影响。

译者补充:setInterval的特性会可能导致出现两个现象(1)有Interval事件被丢弃,这是因为当队列中存在尚未执行的Interval回调时,浏览器不会将新触发的Interval事件加入队列,而是将其丢弃。(2)一大串Interval回调函数没有间隔地顺序执行,之前丢弃事件的机制并没有杜绝这种现象。想象一下当JavaScript线程在执行一个很耗时长的Interval回调函数,此时恰巧队列中没有其他待执行的Interval回调,浏览器还是可以将一个Interval事件回调插入队列中。依此类推,还是有可能出现一大串Interval回调函数没有间隔地顺序执行这一现象(见总结第四点)

最终,当第二个Interval回调函数完成执行的时候,我们可以看到javascript引擎已经没有什么需要执行了。这意味着,浏览器现在正在等待一个新的异步事件的发生。我们可以看到在50ms的时候,Interval回调函数再一次被触发。然而,这一次没有其他代码阻塞它的执行了,Interval回调函数立刻得到了执行。 让我们看一个例子来更好地阐述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在如何执行异步代码上是有本质区别的。 如果timer被正在执行的代码阻塞了,它将会进入队列末尾等待下一次可执行的时间点(可能超过设定的延时时间)。 如果Interval回调函数执行需要花很长时间的话(比指定的Interval延时时间更长),可能会出现一串Interval回调函数没有间隔地顺序执行这一现象。