定时器和计时器

调度:setTimeout和setInterval

 

我们可能决定不立即执行某个功能,但在某个时间之后执行。这叫做“安排一个电话”。

它有两种方法:

  • setTimeout 允许我们在一段时间后运行一次函数。
  • setInterval 允许我们重复运行一个函数,从时间间隔开始,然后以该间隔连续重复。

这些方法不是JavaScript规范的一部分。但是大多数环境都有内部调度程序并提供这些方法。特别是,它们在所有浏览器和Node.js中都受支持。

的setTimeout

语法:

let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)

参数:

func|code
函数或要执行的代码串。通常,这是一个功能。由于历史原因,可以传递一串代码,但不建议这样做。
delay
运行前的延迟,以毫秒(1000毫秒= 1秒)为单位,默认为0。
arg1arg2...
该函数的参数(IE9中不支持)

例如,此代码sayHi()在一秒后调用


function sayHi() {
  alert('Hello');
}

setTimeout(sayHi, 1000);

有参数:


function sayHi(phrase, who) {
  alert( phrase + ', ' + who );
}

setTimeout(sayHi, 1000, "Hello", "John"); // Hello, John

如果第一个参数是一个字符串,那么JavaScript会从中创建一个函数。

所以,这也有效:

 
setTimeout("alert('Hello')", 1000);

但是不推荐使用字符串,使用函数代替它们,如下所示:

 
setTimeout(() => alert('Hello'), 1000);
传递函数,但不要运行它

新手开发人员有时会()在函数后添加括号时出错

// wrong!
setTimeout(sayHi(), 1000);

这不起作用,因为setTimeout需要对函数的引用。这里sayHi()运行函数,并将其执行结果传递给setTimeout在我们的例子中,结果sayHi()undefined(函数不返回任何内容),因此没有安排任何内容。

使用clearTimeout取消

调用setTimeout返回“计时器标识符” timerId,我们可以使用它来取消执行。

要取消的语法:

let timerId = setTimeout(...);
clearTimeout(timerId);

在下面的代码中,我们安排该功能,然后取消它(改变了我们的想法)。结果,没有任何反应:

 
let timerId = setTimeout(() => alert("never happens"), 1000);
alert(timerId); // timer identifier

clearTimeout(timerId);
alert(timerId); // same identifier (doesn't become null after canceling)

正如我们从alert输出中看到的,在浏览器中,计时器标识符是一个数字。在其他环境中,这可能是其他原因。例如,Node.js返回带有其他方法的计时器对象。

同样,这些方法没有通用规范,所以没关系。

对于浏览器,定时器在HTML5标准定时器部分中描述

的setInterval

setInterval方法具有与以下相同的语法setTimeout

let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)

所有论点都有相同的含义。但不像setTimeout它不仅运行一次,而且在给定的时间间隔后定期运行。

要停止进一步通话,我们应该打电话clearInterval(timerId)

以下示例将每2秒显示一条消息。5秒后,输出停止:

 
// repeat with the interval of 2 seconds
let timerId = setInterval(() => alert('tick'), 2000);

// after 5 seconds stop
setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);
时间的推移而alert显示

在大多数浏览器中,包括Chrome和Firefox,内部计时器会在显示时继续“滴答” alert/confirm/prompt

因此,如果您运行上面的代码并且暂时不关闭alert窗口,那么下一步alert将立即显示在您执行此操作时。警报之间的实际间隔将短于2秒。

嵌套的setTimeout

有两种方法可以定期运行。

一个是setInterval另一个是嵌套的setTimeout,如下所示:



/** instead of:
let timerId = setInterval(() => alert('tick'), 2000);
*/

let timerId = setTimeout(function tick() {
  alert('tick');
  timerId = setTimeout(tick, 2000); // (*)
}, 2000);

setTimeout上述安排下一次调用就在当前的结束(*)

嵌套setTimeout是比一种更灵活的方法setInterval这样,可以不同地调度下一个呼叫,这取决于当前呼叫的结果。

例如,我们需要编写一个服务,每隔5秒向服务器发送一个请求数据的请求,但是如果服务器过载,它应该将间隔增加到10,20,40秒......

这是伪代码:

let delay = 5000;

let timerId = setTimeout(function request() {
  ...send request...

  if (request failed due to server overload) {
    // increase the interval to the next run
    delay *= 2;
  }

  timerId = setTimeout(request, delay);

}, delay);

如果我们正在安排的功能是CPU耗尽的,那么我们可以测量执行所花费的时间并迟早计划下一次调用。

嵌套setTimeout允许更精确地设置执行之间的延迟setInterval

让我们比较两个代码片段。第一个使用setInterval

let i = 1;
setInterval(function() {
  func(i);
}, 100);

第二个使用嵌套setTimeout

let i = 1;
setTimeout(function run() {
  func(i);
  setTimeout(run, 100);
}, 100);

对于setInterval内部调度程序将func(i)每100ms 运行一次:

 

你注意到了吗?

func呼叫之间的实际延迟setInterval小于代码!

这是正常的,因为func执行所花费的时间“消耗”了间隔的一部分。

这可能是func的执行结果是比我们预期的时间更长,花的时间超过100毫秒。

在这种情况下,引擎等待func完成,然后检查调度程序,如果时间到了,立即再次运行它

在边缘情况下,如果函数总是执行的时间长于delayms,则调用将完全没有暂停。

这是嵌套的图片setTimeout

 

嵌套setTimeout保证固定延迟(此处为100ms)。

那是因为计划在前一个呼叫结束时进行新呼叫。

垃圾收集和setInterval / setTimeout回调

传入函数时setInterval/setTimeout,会为其创建内部引用并将其保存在调度程序中。即使没有其他引用,它也会阻止函数被垃圾回收。

// the function stays in memory until the scheduler calls it
setTimeout(function() {...}, 100);

对于setInterval函数保留在内存中直到clearInterval被调用。

有副作用。函数引用外部词汇环境,因此,在它存在的同时,外部变量也存在。它们可能比函数本身占用更多的内存。因此,当我们不再需要预定的功能时,最好取消它,即使它非常小。

零延迟setTimeout

有一个特殊的用例:setTimeout(func, 0)或者只是setTimeout(func)

这样func可以尽快安排执行但是调度程序只有在当前执行的脚本完成后才会调用它。

因此该函数被安排在“当前脚本之后”运行。

例如,输出“Hello”,然后立即“World”:

 
setTimeout(() => alert("World"));

alert("Hello");

第一行“在0ms之后将呼叫置于日历中”。但是调度程序只会在当前脚本完成后“检查日历”,因此"Hello"首先,然后"World"- 在它之后。

还有与延迟浏览器相关的零延迟超时用例,我们将在事件循环一章中讨论:微任务和宏任务

零延迟实际上不为零(在浏览器中)

在浏览器中,嵌套计时器运行的频率存在限制。HTML5标准表示:“后5个嵌套计时器,间隔被强制为至少4毫秒。”。

让我们通过下面的例子演示它的含义。其中的setTimeout调用以零延迟重新调度自身。每次调用都会记住times数组中前一次的实时真正的延迟是什么样的?让我们来看看:

 
let start = Date.now();
let times = [];

setTimeout(function run() {
  times.push(Date.now() - start); // remember delay from the previous call

  if (start + 100 < Date.now()) alert(times); // show the delays after 100ms
  else setTimeout(run); // else re-schedule
});

// an example of the output:
// 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100

第一个定时器立即运行(正如规范中所写),然后我们看到9, 15, 20, 24...调用之间的4毫秒强制性延迟发挥作用。

类似的事情发生在我们使用setInterval而不是setTimeout零延迟setInterval(f)运行f几次,然后延迟4+ ms。

这种限制来自古代,许多脚本依赖它,因此它存在是出于历史原因。

对于服务器端JavaScript,该限制不存在,并且存在其他方法来调度立即异步作业,例如setImmediate for Node.js. 所以这个注释是特定于浏览器的。

摘要

  • 方法setTimeout(func, delay, ...args)setInterval(func, delay, ...args)允许我们funcdelay毫秒之后运行一次/定期运行
  • 要取消执行,我们应该clearTimeout/clearInterval使用返回的值调用setTimeout/setInterval
  • 嵌套setTimeout调用是一种更灵活的替代方法setInterval,允许我们更精确地设置执行之间的时间
  • 使用setTimeout(func, 0)(相同setTimeout(func))的零延迟调度用于“尽快,但在当前脚本完成后”调度呼叫。
  • 浏览器将五个或更多嵌套调用的最小延迟限制setTimeoutsetInterval(在第五次调用之后)到4ms。这是出于历史原因。

请注意,所有调度方法都不能保证确切的延迟。

例如,浏览器中的计时器可能会因为很多原因而变慢:

  • CPU过载。
  • 浏览器选项卡处于后台模式。
  • 笔记本电脑是电池。

所有这些都可能将最小定时器分辨率(最小延迟)提高到300ms甚至1000ms,具体取决于浏览器和操作系统级别的性能设置。

任务

重要性:5

编写一个printNumbers(from, to)每秒输出一个数字的函数,从开始到from结束to

制作解决方案的两个变体。

  1. setInterval
  2. 使用嵌套setTimeout

使用setInterval

 
function printNumbers(from, to) {
  let current = from;

  let timerId = setInterval(function() {
    alert(current);
    if (current == to) {
      clearInterval(timerId);
    }
    current++;
  }, 1000);
}

// usage:
printNumbers(5, 10);

使用嵌套setTimeout

 
function printNumbers(from, to) {
  let current = from;

  setTimeout(function go() {
    alert(current);
    if (current < to) {
      setTimeout(go, 1000);
    }
    current++;
  }, 1000);
}

// usage:
printNumbers(5, 10);

请注意,在两种解决方案中,在第一次输出之前都存在初始延迟。1000ms第一次调用该函数

如果我们还希望函数立即运行,那么我们可以在单独的行上添加一个额外的调用,如下所示:


function printNumbers(from, to) {
  let current = from;

  function go() {
    alert(current);
    if (current == to) {
      clearInterval(timerId);
    }
    current++;
  }

  go();
  let timerId = setInterval(go, 1000);
}

printNumbers(5, 10);
重要性:5

在下面的代码中有一个setTimeout调度计划,然后运行繁重的计算,完成时间超过100毫秒。

计划的功能何时运行?

  1. 循环之后。
  2. 在循环之前。
  3. 在循环的开始。

什么alert会显示?

let i = 0;

setTimeout(() => alert(i), 100); // ?

// assume that the time to execute this function is >100ms
for(let j = 0; j < 100000000; j++) {
  i++;
}

任何setTimeout只有在当前代码完成后才会运行。

i会是最后一个:100000000

 
let i = 0;

setTimeout(() => alert(i), 100); // 100000000

// assume that the time to execute this function is >100ms
for(let j = 0; j < 100000000; j++) {
  i++;
}
posted @ 2019-09-12 17:35  阿蒙不萌  阅读(350)  评论(0编辑  收藏  举报