setTimeout 和 setInterval

2020/09/20

这两个方法的用法不应多介绍了,这里主要说说周期性调度的问题。

周期性调度一般有两种方式,一种是使用 setInterval

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

另外一种是嵌套的 setTimeout

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

setInterval 虽然很直观,内部的调度程序每间隔 100 毫秒执行一次 func(i++),但是嵌套的 setTimeout 却有一些 setInterval 不具备的优点。

嵌套的 setTimeout 能够精确地设置两次执行之间的延时

你会发现使用 setInterval 时,func 函数的实际调用间隔要比代码中设定的时间间隔要短,因为 func 自身执行所花费的时间消耗了一部分间隔时间。也可能出现这种情况,就是 func 的执行所花费的时间比我们预期的时间更长,并且超出了 100 毫秒。在这种情况下,JavaScript 引擎会等待 func 执行完成,然后检查调度程序,如果时间到了,则立即执行它。极端情况下,如果函数每次执行时间都超过了设置的延迟时间,那么每次调用之间将完全没有停顿。 但是嵌套的 setTimeout 没有有这样的问题,嵌套的 setTimeout 能确保延时是固定的,这是因为下一次调用是在前一次调用完成时再调度的。

嵌套的 setTimeoutsetInterval 要灵活得多

用嵌套的 setTimeout 可以根据当前执行结果来调度下一次调用,因此下一次调用可以与当前这一次不同。

例如,我们要实现一个服务,每间隔 5 秒向服务器发送一个请求,但如果服务器过载了,那么就要降低请求频率,比如将间隔增加到 10、20、40 秒等,以下是伪代码

let delay = 5000;

let timerId = setTimeout(function request() {

  发送请求 ...

  if (request failed due to server overload) {
    // 下一次执行的间隔是当前的 2 倍
    delay *= 2
  }

  timerId = setTimeout(request, delay)

}, delay)

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

可以用 setTimeout(func, 0) 分割 CPU 高占用的任务

当某项任务执行所消耗时间很长时,有时候会导致浏览器挂起,这种情况是显然不能接受的。

为了方便理解,来看下面这个例子,从 1 数到 1000000000:

let i = 0

let start = Date.now()

function count() {

  // 执行一个耗时的任务
  for (let j = 0; j < 1e9; j++) {
    i++
  }

  alert("Done in " + (Date.now() - start) + 'ms')
}

count()

运行时,会观察到 CPU 挂起,服务器端 JS 表现的尤为明显。如果在浏览器下运行,试试点击页面的其他按钮,你会发现没有反应,因为整个 JavaScript 的执行都暂停了,除非等这段代码运行完,否则什么也做不了。

下面用 setTimeout 分割任务:

let i = 0

const start = Date.now()

function count() {

  // 先完成一部分任务(*)
  while (i % 1e6 !== 0) {
    i++
  }

  if (i == 1e9) {
    alert("Done in " + (Date.now() - start) + 'ms')
  } else {
    setTimeout(count, 0) // 安排下一次任务
  }
}

count()

现在,浏览器的 UI 界面即使在计数正在进行的情况下也能正常工作了,count 函数调用的间隙能够让 JavaScript 引擎缓一口气,浏览器趁这段时间可以对用户的操作作出回应。 而且用 setTimeout 进行分割和不分割这两种做法在执行速度方面几乎没什么差别。