目录

🚠 JavaScript 中的调度函数

在 JavaScript 中,如果不想立即执行一个函数,而是等待一段时间或者间隔执行这个函数,可以使用「计划调用」函数(scheduling a call)。

  • setTimeout :将函数推迟到一段时间间隔之后再执行。
  • setInterval :重复运行一个函数,从一段时间间隔之后开始运行,之后以该时间间隔连续重复运行该函数。

这两个方法并不在 JavaScript 的规范中。但是大多数运行环境都有内建的调度程序,并且提供了这些方法。目前来讲,所有浏览器以及 Node.js 都支持这两个方法。

# setTimeout

使用语法:

let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
1
  • func|code :想要执行的函数或代码字符串。 一般传入的都是函数。(不建议传入代码字符串)
  • delay :执行前的延时,以毫秒为单位(1000 ms = 1s),默认值是 0;
  • arg1arg2 … :要传入被执行函数(或代码字符串)的参数列表。

🌰 例子 :

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

setTimeout(sayHi, 1000);
1
2
3
4
5

注意,传入一个的是 函数的名称, 而不要在后面加上 () 成为传入函数的执行结果。

🌰 例子 / 带参数的情况:

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

setTimeout(sayHi, 1000, "Hello", "John");
1
2
3
4
5

🌰 例子 / 使用箭头函数避免使用字符串形式:

setTimeout(() => alert('Hello'), 1000)
1

使用 clearTimeout 取消 setTimeout 调度,一般在使用 setTimeout 接受一个 timerId

let timer = setTimeout(...);
clearTimeout(timerId);
1
2

🌰 例子 :

let timerId = setTimeout(() => alert("never happens"), 1000);
alert(timerId); // 定时器标识符

clearTimeout(timerId);
alert(timerId); // null
1
2
3
4
5

# setInterval

setTimeout 的语法使用相同:

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

🌰 例子 / 每间隔两秒就输出一条消息:

// 每 2 秒重复一次
let timerId = setInterval(() => alert('tick'), 2000);

// 5 秒之后停止
setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);
1
2
3
4
5

提示

alert 弹窗显示的时候计时器依然在进行计时。在大多数浏览器中,包括 Chrome 和 Firefox,在显示 alert/confirm/prompt 弹窗时,内部的定时器仍旧会继续 “嘀嗒”。

所以,在运行上面的代码时,如果在一定时间内没有关掉 alert 弹窗,那么在你关闭弹窗后,下一个 alert 会立即显示。两次 alert 之间的时间间隔将小于 2 秒。

# 嵌套 setTimeout

🌰 例子 / 使用 setTimeout 同样可以实现周期性调用:

let timerId = setTimeout(function tick() {
  alert('tick');
  timerId = setTimeout(tick, 2000);
}, 2000);
1
2
3
4

嵌套的 setTimeout 要比 setInterval 灵活得多。采用这种方式可以根据当前执行结果来调度下一次调用,因此下一次调用可以与当前这一次不同。

🌰 例子 / 根据上一次的执行结果来执行下一次调用:

let delay = 5000
let timerId = setTimeout(function request(){
  // ... 发送请求
  
  if ( ... ) { // ... 服务器过载发送请求失败 
      delay *= 2
  }
  
  timerId = setTimeout(request, delay)
}, delay)
1
2
3
4
5
6
7
8
9
10

实现一个服务(server),每间隔 5 秒向服务器发送一个数据请求,但如果服务器过载了,那么就要降低请求频率,比如将间隔增加到 10、20、40 秒等。

并且,如果调度的函数占用大量的 CPU,那么我们可以测量执行所需要花费的时间,并安排下次调用是应该提前还是推迟。

嵌套的 setTimeout 能够精确地设置两次执行之间的延时,而 setInterval 却不能。

🌰 例子:

  • setInterval

    let i = 1
    setInterval(function() {
      func(i++);
    }, 100);
    
    1
    2
    3
    4
  • 使用嵌套 setTimeout

    let i = 1;
    setTimeout(function run() {
      func(i++);
      setTimeout(run, 100);
    }, 100);
    
    1
    2
    3
    4
    5

setInterval 而言,内部的调度程序会每间隔 100 毫秒执行一次 func(i++)使用 setInterval 时, func 函数的实际调用间隔要比代码中设定的时间间隔要短。 因为 func 的执行所花费的时间也算进了时间间隔中。

在这种情况下,JavaScript 引擎会等待 func 执行完成,然后检查调度程序,如果时间到了,则 立即 再次执行它。极端情况下,如果函数每次执行时间都超过 delay 设置的时间,那么每次调用之间将完全没有停顿。

image-20220511133813749

对于 setTimeout ,** 嵌套的 setTimeout 就能确保延时的固定(这里是 100 毫秒)。** 因为下一次调用是在前一次调用完成时再调度的。

image-20220511133957352

提示

垃圾回收和 setInterval / setTimeout 回调 (callback):当一个函数传入 setInterval / setTimeout 时,将为其创建一个 内部引用,并保存在调度程序中。这样,即使这个函数没有其他引用,也能防止垃圾回收器(GC)将其回收

// 在调度程序调用这个函数之前,这个函数将一直存在于内存中
setTimeout(function() {...}, 100);
1
2

但是 如果函数引用了外部变量(闭包),那么只要这个函数还存在,外部变量也会随之存在。它们可能比函数本身占用更多的内存。因此,当不再需要调度函数时,最好取消它,即使这是个(占用内存)很小的函数。

# 零延时的 setTimeout

setTimeout 的特殊用法: setTimeout(func, 0) ,或者仅仅是 setTimeout(func)

这样调度函数,可以让 func 尽快执行,但是只有在当前正在执行的脚本执行完成后,调度程序才会调用它。也就是说,该函数被调度在当前脚本执行完成「之后」立即执行。

🌰 例子:

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

alert("Hello");
1
2
3

这段代码会先输出 Hello ,然后立即输出 World

setTimeout 将调度安排到了 日程 0 毫秒处。但是调度程序只有在当前脚本执行完毕时才会去「检查日程」,所以先输出 Hello ,然后才输出 World

点击查看

在浏览器环境中零延时实际上不为零:在浏览器环境下,嵌套定时器的运行频率是受限制的。根据 HTML5 标准 (opens new window) 所讲:「经过 5 重嵌套定时器之后,时间间隔被强制设定为至少 4 毫秒」。

🌰 例子 / setTimeout 调用会以零延时重新调度自身的调用。每次调用都会在 times 数组中记录上一次调用的实际时间。:

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

setTimeout(function run() {
  times.push(Date.now() - start); // 保存前一个调用的延时

  if (start + 100 < Date.now()) alert(times); // 100 毫秒之后,显示延时信息
  else setTimeout(run); // 否则重新调度
});

// 输出示例:
// 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100
1
2
3
4
5
6
7
8
9
10
11
12

在代码的第一次执行时,定时器是立即执行的。但接下来的调用,必须经过 4 毫秒以上的强制延时。 timer 数组里存放的是每次定时器运行的时刻与 start 的差值,所以数字只会越来越大,实际上前后调用的延时是数组值的差值。

对于 setInterval 也会发生类似的情况: setInterval(f) 会以零延时运行几次 f ,然后以 4 毫秒以上的强制延时运行。

对于服务端的 JavaScript,就没有这个限制,并且还有其他调度即时异步任务的方式。例如 Node.js 的 setImmediate (opens new window)。因此,这个提醒只是针对浏览器环境的。

📢 上次更新: 2022/09/02, 10:18:16