原文链接:https://johnresig.com/blog/how-javascript-timers-work/
JavaScript 定时器工作原理是一个重要的基础知识点。因为定时器在单线程中工作,它们表现出的行为很直观。
我们该如何创建和维护定时器呢?要从如下三个函数(都是定义在全局作用域,在浏览器中就是 Window 的方法)说起:
-
var id=setTimeout(fn,delay);
初始化一个只执行一次的定时器,这个定时器会在指定的时间延迟delay
之后调用函数fn
,该setTimeout
函数返回定时器的唯一id
,我们可以通过这个id
来取消定时器的执行。 -
var id=setInvertal(fn,delay);
与setTimeout
类似,只是它会以delay
为周期,反复调用函数fn
,直到我们通过id取消该定时器。 -
clearInterval(id),clearTimeout(id);
这两个函数接受定时器的id
(例如我们上面提到的两个函数产生的定时器 id ),并停止对定时器中指定函数的调用。
要深入理解定时器工作原理,我们需要探索一个重要的概念:定时器指定的延迟时间并不能得到保证。
在浏览器中,因为所有的 JavaScript 代码都运行在单一线程之中,异步事件(如鼠标点击,定时器)只有在他们被触发的时候他们的回调才有机会得以执行。
我们可以用下图说明:
图中包含大量的信息,吸收并理解这些信息,能帮助我们领悟“异步的 JavaScript 代码是如何工作的”。
这个图是一维的,垂直方向是时间,以毫秒为单位。蓝色的盒子代表正在执行的javascript代码所占时间片段。
例如,第一个 JavaScript 块执行时间约 18ms,第二个鼠标点击块执行了约 11ms,其他块类似。
因为单线程的缘故,在同一时间只能执行一条 JavaScript 代码,每一个代码块(蓝色盒子)都会阻塞其他异步事件的执行。
这就意味着,当一个异步事件发生的时候(例如鼠标点击,定时器触发,一个 XMLHttpRequest
请求完成),它进入了代码的执行队列,执行线程空闲时会依照该执行队列中顺序依次执行代码。(如何将异步事件加入队列,不同浏览器,他们的实现可能有所差异,所以这里我们将其简单化)。
开始的时候,在 JavaScript代码块(第一个盒子),初始化了两个定时器,一个 10ms 延迟的 setTimeout
和 10ms 的 setInterval
。这些定时器可能会在我们第一个代码块执行结束之前就触发,这取决于定时器在第一个代码块中启动的位置和时间。
注意,定时器虽然触发了,但是并不会立即执行,它只是把需要延迟执行的函数加入了执行队列,在线程的某一个可用的时间点,这个函数就能够得到执行。
当第一个 JavaScript 代码初始化块执行结束,浏览器立即提出一个问题:谁在等待着被执行?
在这个案例中鼠标点击时间的处理程序和一个定时器( setTimeout
)都在等待。浏览器选择一个并执行(这里是鼠标点击事件的处理程序)。定时器就需要等待下一个可用时间来执行。
需要注意的是当鼠标点击事件处理程序执行的时候,第一个 interval
定时器触发了。和 timeout
定时器一样,他的回调函数被加入了执行队列,等待执行。
然而,还需要注意到当 interval
定时器再次触发,这个时候 timeout
定时器的回调函数正在执行,此时这个 interval
的触发被放弃了。
假想(浏览器不这样做),在一个占用时间很多的初始化定时器的代码块中,所有的 interval
触发都把回调加入执行队列,当初始化代码块结束后,执行队列中已经累加了大量的定时器回调函数,结果就会出现大量的 interval
回调函数无间隔的执行,直到该执行队列清空。所以浏览器在讲一个 interval
回调加入执行队列前,会检查执行队列,如果其中存在尚未执行的 interval
回调那么就等待,直到当前执行队列中没有相应 interval
的回调以后才会继续入队 interval
回调。
事实上,如图,我们看见在第一个 interval
的回调执行的时候(之前进入执行队列),第三个 interval
触发了,这想我们展示一个重要的现象: interval
不关心当前正在执行的代码,他们会不加选择的添加回调到执行队列,尽管这意味着两个 interval
回调函数执行的时间间隔被牺牲。这里第一个 interval
回调执行结束后,紧跟着第三个 interval
的回调马上得到执行,中间没有印象中应该有的 10ms 间隔。
最终,在第三个 interval
的回调执行结束后,我们看见执行队列中没有等待 JavaScript 引擎执行的代码,这就意味着,浏览器现在等待新的异步事件的发生,在 50ms 的刻度处 interval
再次触发,此时没有什么会阻塞 JavaScript 引擎,这个 interval
回调会立即执行。
让我们看一个例子来阐明,setInterval
和 setTimeout
的不同:
setTimeout(function () { /* Some long block of code... */ setTimeout(arguments.callee, 10); }, 10); setInterval(function () { /* Some long block of code... */ }, 10);
看第一眼,会觉得这两段代码功能相同,实际上,他们是不同的。
需要注意到, setTimeout
的回调函数的执行总是保证了至少 10ms 的间隔(与上一个回调的执行相比,实际执行时,这个间隔可能变长,但是不可能更少),但是 setInterval
会尝试每隔 10ms 执行一次回调,不管上一个回调函数时候已经执行完毕。(很多类库的动画都是使用的 setTimeout
实现)
这里我们学到很多,总结一下:
-
JavaScript 引擎是单线程的,会迫使异步事件进入执行队列,等待执行。
-
setTimeout
和setInterval
在执行异步代码时从根本上是有所不同的。 -
如果一个定时器事件被阻塞,使得它不能立即执行,那么它会被延迟,直到下一个可能的时间点,才被执行(这可能比你指定的
delay
时间要长) -
Interval
的回调有可能‘背靠背’无间隔的执行,这种情况是说interval
的回调函数的执行时间比你指定的delay
时间还要长
这些都是构建 JavaScript 应用程序非常重要的知识。了解 JavaScript Engine 是如何工作的,特别存在大量的异步事件发生,为构建高级应用程序代码打下基础。
未经允许不得转载:w3h5 » JavaScript中定时器的工作原理(How JavaScript Timers Work)
原创文章,作者:Maggie-Hunter,如若转载,请注明出处:https://blog.ytso.com/150427.html