Skip to main content
 首页 » 编程设计

Javascript 调度: setTimeout and setInterval

2022年07月19日167虾米姐

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 identifier 
 
clearTimeout(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 seconds 
let timerId = setInterval(() => alert('tick'), 2000); 
 
// after 5 seconds stop 
setTimeout(() => { 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 it 
setTimeout(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取决于浏览器和设置。


本文参考链接:https://blog.csdn.net/neweastsun/article/details/73843584