Javascript 调度: setTimeout and setInterval

来源:互联网 发布:mac音乐转换格式软件 编辑:程序博客网 时间:2024/06/11 02:03

Javascript 调度: setTimeout and setInterval

我们可能决定不立刻执行一个函数,而是某时间之后执行,一般我们称为“调度执行”。
有两种方法可以实现:

  • setTimeOut指某时间间隔之后执行一次函数。
  • setInterval指安装一定时间间隔有规律执行函数。

这些函数不是Javascript规范的一部分,但是大多数环境有内部调度并提供了这些方法。特别是,所有浏览器和Node.Js都支持。

setTimeout

语法:

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

参数

func|code
被执行的函数或代码字符串,通常是函数,因为历史原因,代码字符串被允许,但不建议使用。

delay
运行之前的时间数,毫秒为单位。

arg1,arg2...
被执行函数的参数(IE9以下不支持)

举例,下面代码1秒之后执行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()是函数的执行返回值,即表达式的执行结果传递给函数。这里sayHi()返回值为undefined(函数返回值为空),所以没有啥需要调度。

使用clearTimeout取消调度

调用setTimeout返回一个“timer标识符”timerId,我们使用使用其取消调度执行,语法为:

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

下面代码中,我们调度函数,然后取消调度,所以结果什么都没有发生:

let timerId = setTimeout(() => alert("never happens"), 1000);alert(timerId); // timer identifierclearTimeout(timerId);alert(timerId); // same identifier (doesn't become null after canceling)

我们能看到alter输出,在浏览器中时间标识符为数字,其他环境可能为其他值,如NodeJS中返回时间对象,并带有额外的方法。

因为没有统一的规范,所以也没有问题。

对浏览器,timer在html5中描述了规范。

setInterval

方法setInterval语法与setTimeout一致:

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

所有参数意思相同,但与setTimeout不同的是,其运行函数不只一次,而是有规律地根据某个时间间隔运行。

为了停止运行,可以调用clearInterval(timerId)
下面示例每个2秒显示消息,5秒过后,输出停止:

// repeat with the interval of 2 secondslet timerId = setInterval(() => alert('tick'), 2000);// after 5 seconds stopsetTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);

Chrome/Opera/Safari中模态窗口冻结时间

在ie或firefox浏览器中,当显示alert/confirm/prompt时,内部时钟继续计时,但是在Chrome/Opera/Safari中内部时钟却冻结。

所以如果你运行上面代码,不关闭alter窗口一段时间,那么Firefox/IE 下面一个alert将立即显示(因为前面的执行也在计时),而Chrome/Opera/Safari则超过2秒才执行后面代码(因为alert出现时,内部计时冻结)。

递归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(tick, 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内部执行将每100ms运行func(i)一次:

你注意到了吗?

对Interval方式的func函数调度实际间隔时间少于100毫秒!

显然,因为执行func本身需要消费一部分间隔时间。可能func执行时间长于我们预期时间,甚至超过100号秒。

如果不超过100毫秒,引擎等待func执行完成,然后检查调度,如果时间到了,立刻再次运行。另一种情况,函数执行总是超过间隔时间,那么函数将根本没有暂停立刻执行。

下图是递归setTimeout方式运行图:

递归setTimeout方式确保固定时间间隔

因为新的调用被计划在前一个执行之后。

垃圾回收
当函数被传递给setInterval/setTimeout,有一个内部引用被创建指向它并保留在调度中,其阻止函数被垃圾回收,即使没有其他引用指向它。

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

对于setInterval,函数一直驻留内存,直到cancelInterval被调用。这样有些副作用,函数引用外部词法环境,所以一直激活,外部变量也是。因此需要比函数自身消耗的内存更多。所以,当我们不需要调度函数时,最好取消它,即使其很小。

setTimeout(…,0)

有个特别的用途:setTimeout(func,0),调度函数func尽可能快地执行,但调度将在当前代码完成之后执行。

所以函数调度正是运行在当前代码之后,也就是说,异步执行,下面示例先输出“hello”,然后立刻输出“World”。

setTimeout(() => alert("World"), 0);alert("Hello");

第一行代码设置调用为0ms之后,但是调度仅在当前代码完成之后开始检查时钟,所以“hello”先输出,然后是“world”。

分割耗CPU任务

巧妙使用setTimeout可以分割耗CPU任务。举例,语法高亮脚本(通常给某页一些示例代码颜色化)是相对耗CPU的任务。为了高亮代码,需要执行分析,创建很多演示元素,并增加他们至页面文档中,对大量文本来说,需要很长时间,这可能引起浏览器挂起,用户无法接受。

所以我们能分割长文本为多个块,首先第一个100行,然后使用setTimeout(...,0)计划另一个100行,一直往复。

为了描述清除,我们举个简单的示例,有个函数需要计数,从1到1000000000。

如果你直接运行,cpu将挂起,对服务器段脚本也是很明显,如果你运行在浏览器段,试图点击页面上的其他按钮,会发现整个Javascript完全停滞,我都知道代码执行完成才会有反应。

let i = 0;let start = Date.now();function count() {  // do a heavy job  for(let j = 0; j < 1e9; j++) {    i++;  }  alert("Done in " + (Date.now() - start) + 'ms');}count();

浏览器可能会显示“脚本耗时太长”警告(希望没有,数字并不是很大)。让我们分割任务,使用嵌套的setTimeout:

let i = 0;let start = Date.now();function count() {  // do a piece of the heavy job (*)  do {    i++;  } while (i % 1e6 != 0);  if (i == 1e9) {    alert("Done in " + (Date.now() - start) + 'ms');  } else {    setTimeout(count, 0); // schedule the new call (**)  }}count();

现在浏览器ui在“count”执行过程中功能正常。我们主要做了几部分工作:

  1. 首先运行 : 1…1000000.
  2. 第二次运行: 1000001…2000000.
  3. …往复运行,如果while检查i正好能被1000000整除。

如果还没有完成,下一个调用被调度(*号行)

count之间的暂停执行让Javascript引擎有了喘息时间,正好可以执行其他任务,用于响应用户动作。

值得注意的是:两种方式:采用和不采用setInterval分割任务,在速度上有比较,整个计数时间没有差异。

为了说明更清楚,让我们改进示例,我们把调度代码移动到count的开始处:

let i = 0;let start = Date.now();function count() {  // move the scheduling at the beginning  if (i < 1e9 - 1e6) {    setTimeout(count, 0); // schedule the new call  }  do {    i++;  } while (i % 1e6 != 0);  if (i == 1e9) {    alert("Done in " + (Date.now() - start) + 'ms');  }}count();

现在,当我们开始count时,发现需要更多的count,在执行任务之前调度立刻执行。
如果你运行程序,很容易发现耗时减少。

浏览器内置定时器最小延迟

浏览器中,对内置时钟运行的频率有限制,HTML5标准描述:5个嵌套时钟之后。。。间隔被迫需要4毫秒。

下面示例演示说明,setTimeout被调度0ms后开始运行,每次调用记录实际运行时间,我们看看延迟情况:

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, 0); // else re-schedule}, 0);// 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。。。和规范中写的一致。

这种限制来自旧版本,许多脚本依赖它,因为历史原因而存在。

服务器段Javascript,没有这样的限制,也有其他方式调度立刻执行的异步任务,如process.nextTick 和 setImmediate 在 Node.js中。因此只有浏览器才有这样概念。

浏览器渲染过程

因为浏览器通常脚本执行完成后,全部渲染给用户,利用延迟技术可以给用户显示精度条或其他方式的过程。

我们有一个巨大的函数,其改变某对象,改变直到执行完成才反应出来。示例如下:

<div id="progress"></div><script>  let i = 0;  function count() {    for(let j = 0; j < 1e6; j++) {      i++;      // put the current i into the <div>      // (we'll talk more about innerHTML in the specific chapter, should be obvious here)      progress.innerHTML = i;    }  }  count();</script>

如果你运行代码,i的值是在整个代码执行完成后才显示。

如果我们使用setTimeout去分割代码,修改值被分配至每个片段中,效果更好:

<div id="progress"></div><script>  let i = 0;  function count() {    // do a piece of the heavy job (*)    do {      i++;      progress.innerHTML = i;    } while (i % 1e3 != 0);    if (i < 1e9) {      setTimeout(count, 0);    }  }  count();</script>

现在div中的值不断在增长。

总结

  • 两个方法 setInterval(func, delay, ...args)setTimeout(func, delay, ...args)都可以实现定时运行函数。
  • 取消调度执行,可以调用 clearInterval/clearTimeout ,使用 setInterval/setTimeout函数的返回值.
  • 嵌套 setTimeout调用比 setInterval更灵活. 也能保证执行之间的最小时间(异步方式).
  • 零时间调度setTimeout(...,0)用于调度执行 “尽可能短, 但必须在当前代码执行完成之后”.

使用 setTimeout(...,0)的场景有:

  • 分割耗时CPU任务至多个片段,避免执行代码时程序挂起。
  • 为在执行任务的同时,让浏览器可以执行其他任务(绘制进度条).

注意,所有调度方法不能保证精确的延迟时间,我们不能依赖调度代码实现。

举例,浏览器内置时钟慢可能有许多原因:

  • CPU超负荷.
  • 浏览器 tab 是后台模式.
  • 笔记本电脑电池.

所有可能降低最小计时器方案(最小延迟)300毫秒甚至1000 ms取决于浏览器和设置。

原创粉丝点击