定时器(interval)与延时器(timeout)

定时器与延时器提供了一种让一段代码在一定毫秒之后,再异步执行的能力。由于 JavaScript 单线程的特性,定时器与延时器提供了一种跳出这种限制的方法,以一种不太直观的方式来执行代码。

有一个需要理解的重要概念,就是 JavaScript 中定时器与延时器的执行时间是不能保证的,原因就是因为 JavaScript 是单线程的。JavaScript 在同一时间只能执行一个代码块,这些代码块的执行就阻塞了异步事件的处理。这意味着,当一个异步事件发生时(如鼠标单击,ajax 回调事件),它就会排队,并且在线程空闲时才进行执行。

很容易验证浏览器对定时器或延时器的阻塞,使用alert()即可,代码很简单:

运行结果取决于何时点击对话框的“确定”按键,我为了效果明显,等了大概十几秒:
block-timeout

定时器(interval)与延时器(timeout)之间的区别

初看之下,定时器可能像是周期性执行的延时器一样。但是他们的不同之外要更多一些。通过下面的代码对比:

上面这段代码功能似乎是相同的,实际上不是。注意:setTimeout()代码中,要在前一个 callback 回调执行结束并延迟一秒(可能更多,但不会更少)以后,才能再次执行setTimeout()。而setInterval()则是每隔一秒就尝试执行 callback 回调,而不关注上一个 callback 是何时执行的。

延时器回调从来不保证执行的具体时间。它并非像定时器那样每隔一秒触发一次,而是在执行之后重新设置延时器,一秒后再次触发。所以关于定时器与延时器,要记住以下几点:

  • JavaScript 引擎是单线程执行,异步事件必须要排队等待才能执行。
  • 如果无法立即执行定时器或者延时器,该定时器或延时器会被推迟到下一个可用的执行时间点上(可以更长,但不会比指定的延迟时间更少)。
  • 如果一直被延迟,到最后,定时器(interval)可能会无延迟执行(许多回调被挤在一起),并且同一个定时器处理程序的多个实例不能同时进行排队。
  • setTimeout()setInterval()在触发周期的定义上是完全不同的。
利用延时器处理昂贵的计算过程

JavaScript 的单线程本质可能是 JavaScrip 复杂应用程序开发中的最大“陷阱”。在 JavaScript 执行繁忙的时候,浏览器中的用户交互,最好的情况是操作稍有稍有缓慢,最差的情况则是反应迟钝。这可能会导致浏览器很卡或者崩溃,因为在 JavaScript 执行的时候,页面渲染的更新操作都要暂停。

因此,如果要保持界面有良好的响应能力,减少运行时间超过几百毫秒的复杂操作,将其控制在可管理状态是非常有必要的。此外,如果一段代码运行时间超过 5 秒,有些浏览器(比如 Firefox 和 Opera)将弹出一个对话框警告用户该脚本“无法响应”。而其它浏览器,比如 iPhone 上的浏览器,将默认终止运行时间超过 5 秒的脚本。

在处理大量数据的时候,比如操作上千个 DOM 元素,经常会产生不响应的用户界面。这个时候,延时器(timeout)就变得非常有用了。作为延时器,它在一段时间之后,可以有效暂停一段 JavaScript 代码的执行,延时器还可以将代码的各个部分,分解成不会让浏览器挂掉的碎片。

考虑到这一点,我们可以将强循环和操作转化为非阻塞操作,如下的示例代码,需要很长一段时间:

本例中,我们创建了 140000 个DOM节点,并使用大量的单元格来填充一个表格。这是非常昂贵的操作,明显会增加浏览器的执行时间,从而阻止正常的用户交互操作。

我们需要做的是,使用延时器定期让代码暂停,以便能即时的将结果输出,示例代码如下:

在该修订版本中,我们将冗长的操作拆分成五步(根据实际情况设置)小操作,每个操作创建自己的 DOM 节点。这些较小的操作,则不太可能让浏览器崩溃。这是有要注意的地方,我们需要将跟踪上一次迭代中断的地方(#2),以及如何自动安排下一个迭代,一直到全部结束(#3)。

为了适应这种新的异步方式,我们只需要改动很少一部分代码。我们还需要做一些额外的工作,来跟踪执行的代码。确保操作正确进行,以及调度其它执行部分。除些之外,核心代码和之前的代码,看起来很类似。

从用户角度来看,这种技术带来的最明显的变化是原本很卡的浏览器页面,换成了五部分可以进行视觉化更新的页面。尽管浏览器会尝试尽快执行我们的代码段,但在每一个定时器执行之后,浏览器还是会渲染 DOM 更新的。而在初始版本的代码中,需要等待一次大的批量更新。

最重要的是:这种技术提供了一种使用延时器解决浏览器单线程限制的方法。

中央延时器|定时器控制

使用延时器可能出现的问题是对大批量延时器的管理。这在处理动画时尤其重要,因为在试图操纵大量属性的同时,我们还需要一种方式来管理它们。

管理多个延时器或定时器会出现很多问题,原因有很多。存在的问题不仅是要保留大量定时器的引用,然后迟早还必须消灭它们(尽管可以用闭包来管理),而且还干扰了浏览器的正常运行。我们之前看到,确保定时器处理程序不执行过于冗长的操作,可以防止我们的代码阻塞其它操作,但也有一些其它浏览器已经考虑这方面了。其中之一就是垃圾回收。

同时创建大量的延时器或定时器,将会在浏览器中增加垃圾回收任务发生的可能性。有些浏览器可以很好的处理这种情况,但其它一些浏览器的垃圾回收周期则很长。可能一个动画在某个浏览器中很流畅,换一个浏览器却很卡顿。减小同时使用延时器或定时器的数量,将大大有助于解决这种问题,这就是为什么所在现代动画引擎都使用一种被称之为中央延时器控制(central timer control)的技术。

在多个延时器中使用中央延时器控制,可以带来很大的威力和灵活性。

  • 每个页面在同一时间只需要运行一个延时器
  • 可以根据需要暂停和恢复延时器
  • 删除回调函数的过程变得很简单

以下例子使用该技术来管理多个函数,被管理的这些函数分别操作不同的动画属性。首先,创建一个延时器以及用于管理多个处理程序的函数:

在以上代码中,我们创建了一个中央控制结构(#1,timers对象),我们可以在该结构上添加任意数量的延时器回调函数,而且还可通过它,启动和停止该结构的执行。此外,在任何时候如果 callback 回调函数返回了false值,都允许将其删除,这比典型的clearTimeout()更方便。

让我们大致分解下代码:

  • #2 所有的回调函数都存储于一个名为timers的数组中,还包括当前延时器的 Id 。这些变量是延时器唯一需要维护的内容
  • #3 add()方法接受一个 callback 回调,并简单将其添加到timers数组中。
  • #4 核心方法start()。在该方法内,首先确认没有延时器在运行(能过检查timerId是否有值),如果确认没有延时器在执行,立即执行一个即时函数来开启中央延时器。

在即时函数内,如果注册了处理程序就遍历执行每个处理程序。如果有处理程序返回false值,我们就从数组中将其删除,最后进行下一次调度。

我们用一个元素动画来演示中央延时器的使用:

演示及源码地址:http://runjs.cn/code/ynl0klmw

以这种方式组织延时器,可以确保回调函数总是按照添加的顺序进行执行。而普通的延时器通常不会保证这种顺序,有可能后面的一个处理程序在前面就执行了。

发表评论

电子邮件地址不会被公开。 必填项已用*标注