JavaScript 的计时器的工作原理

发布于 2021-01-13 13:21:28 字数 3115 浏览 1203 评论 1

原文链接:https://johnresig.com/blog/how-javascript-timers-work/

从基础层面来讲,理解 JavaScript 计时器的工作原理是很重要的。由于 JavaScript 是单线程的,所以很多时候计时器并不是表现得和我们的直观想象一样。让我们从下面的三个函数开始,它们能够让我们有机会去构造和操作计时器。

  • var id  =setTimeout(fn, delay); 创建了一个简单的计时器,在经过给定的时间后,回调函数将会被执行。这个函数会返回一个唯一的ID,便于在之后某个时间可以注销这个计时器。
  • var id = setInterval(fn, delay); 和 setTimeout 类似,但是每经过一段时间(给定的延时),所传递的函数就会被执行一次,直到这个定时器被注销。
  • clearInterval(id);clearTimeout(id); 接受一个计时器ID(由之前两种计时器返回)并且停止计时器回调函数的执行。

为了理解计时器的内部工作原理,我们首先需要了解一个非常重要的概念:计时器设定的延时是没有保证的。因为所有在浏览器中执行的JavaScript单线程异步事件(比如鼠标点击事件和计时器)都只有在它有空的时候才执行。这最好通过图片来说明,就如下面这张图所示:

这一张图片里面有很多信息需要慢慢消化,但是彻底地理解这张图片将会让你对 JavaScript 异步执行是如何工作的有一个更好的认识。这张图片是从一维的角度来阐述的:在垂直方向是以毫秒计的时间,蓝色的块代表了

当前正在执行的 JavaScript 代码段。比如第一段 JavaScript 执行了大概 18 毫秒,鼠标点击事件大概执行了11毫秒。

由于 JavaScript 每次只能执行一段代码(基于它单线程的特性),所以所有这些代码段都阻塞了其他异步事件的执行。这就意味着,当一件异步事件(比如鼠标点击,计时器触发和一个 XMLHttpRequest 请求完成)触发的时候,这些事件的回调函数将排在执行队列的最后去等待执行(排队的方式因浏览器不同而不同,这里只是一个简化)。

一开始,在第一段代码段内,两个计时器被初始化:一个 10ms 的 setTimeout 和一个 10ms 的 setInterval。由于计时器在哪儿初始化就在那儿开始计时,所以实际上计时器在第一段代码执行完成之前就触发了。然而,计时器的回调函数并不是立即执行了(单线程限制了不能这样做),相反的是,回调函数排在了执行队列的最后,等到下一个有空的时间去执行。

此外,在第一个代码块内我们看到了一个鼠标点击事件发生了。与之相关的javascript异步事件(我们不可能预测用户会在什么时候去采取这样的动作,因此这个事件被视为异步的)并不会立即执行。和计时器一样的是,它被放到了队列的最后去等待执行。

在第一个代码快执行完成的时候,浏览器会立即发出这样的询问:谁正在等待执行?这个时候,鼠标点击处理程序和计时器回调函数都在等待执行。浏览器选择了其中一个(鼠标点击回调函数)并且立即执行它。为了执行,计时器会等到下一个可能执行的时间。

我们注意到,当鼠标点击事件对应的处理程序正在执行的时候,第一个定时回调函数也要执行了。同定时计时器一样,它也在队列的后面等待执行。然而,我们可以注意到,当定时器再一次触发(在计时器回调函数正在执行的时候),这一次定时器回调函数被丢弃了。如果在执行一大块代码块的时候,你把所有的定时回调函数都放在队列的最后,结果就是一大串定时回调函数将会没有间隔的一起执行,直到完成。相反,在把更多定时回调函数放到队列之前,浏览器会静静的等待,知道队列中的所有定时回调函数都执行完成。

事实上,我们可以看到,当 interval 回调函数正在执行的时候,interval 第三次被触发。这给我们一个很重要的信息:interval并不关心当前谁在执行,它的回调函数会不加区分地进入队列,即使存在这个回调函数会被丢弃的可能。

最后,当第二个定时回调函数完成执行的时候,我们可以看到 JavaScript 引擎已经没有什么需要执行了。这意味着,浏览器现在正在等待一个新的异步事件的发生。我们可以看到在 50ms 的时候,定时回调函数再一次被触发。然而这一次,没有其他代码阻塞他的执行了,所以他立即执行了定时回调函数。

让我们看一个例子来更好地阐述 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 是有着本质区别的。
  • 如果计时器被正在执行的代码阻塞了,它将会进入队列的尾部去等待执行直到下一次可能执行的时间出现(可能超过设定的延时时间)。
  • 如果 interval 回调函数执行需要花很长时间的话,比指定的延时长,interval 有可能没有延迟背靠背地执行。

上述这一切对于理解 JS 引擎是如果工作的无疑是很重要的知识,尤其是大量的典型的异步事件发生时,对于构建一个高效的应用代码片段来说是一个非常有利的基础。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

虐人心 2021-01-13 16:07:15 1 楼

说的很对,所以永远不要相信前端的定时器,需要定期去服务端同步时间,或者计算一个时间差从前端本地获取时间。

~没有更多了~

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

0 文章
0 评论
84935 人气
更多

推荐作者

待"谢繁草

文章 0 评论 0

战皆罪

文章 0 评论 0

子英

文章 0 评论 0

爱的十字路口

文章 0 评论 0

孤者何惧

文章 0 评论 0

xi霄xi

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击“接受”或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。