JavaScript 中的 setTimeout 和 setInterval 中的时间是怎么控制的

比如说,我写了一个5秒的定时器或延时器,js和浏览器是怎么控制这个时间的,也就是为什么5秒钟以后才执行,它的内部是怎么实现的,为什么5秒钟之前不会执行,这个时间是怎么控制的,特别想知道这个问题,网上也找不到相应的答案

阅读 3.8k
6 个回答

个人理解,仅供参考:
js是单线程模式,按理说就是按顺序来执行代码块,放到任务队列中去执行,所以是存在阻塞这个概念的,也就是说上一个代码段执行完了才会执行下一个。但是定时器以及Ajax之类是异步执行,似乎有点多线程的感觉了,但实际上在JS中还是单线程,不过浏览器是有多个线程的。所以定时器在设定以后,也会放入任务队列中去,但是是在你设定的延迟时间后才执行。至于楼主说的定时器的工作原理,应该是比较底层的东西,才学浅薄,不太了解,不好意思。楼主可以参考下面这个图,js引擎线中的执行顺序。

clipboard.png

是定时器搞得鬼。JavaScript引擎都有一个定时器timer,当调用setTimeout时,JS引擎会在设定的ms后将传入的函数放入事件队列,排队等待主线程调用。

这里涉及的知识包括线程、同步、异步、事和件循环。由于题主关注的问题在于如何控制延迟时间,所以这些问题不在赘述,不过这些知识点值得深入学习。

简单理解的话:
js是单线程的,定时器里面的方法(以及其他异步方法,包括ajax回调,promise的then)会放入事件栈,只有当线程里面的代码全部执行完之后才会去从事件栈中取出相应达到执行条件的方法放入处理线程中,处理完后再去事件栈中取,不断循环。因此实际上延迟的5秒并不是准确的时间,它只是在处理完当前线程里的事务(有可能6秒才处理完)再从事件栈中取(判断是否已经超过了5秒)

我是不知道v8引擎是怎么实现的,要知道具体细节得去看文档和源码。本来想写在评论里但感觉我要说的应该还有点参考价值emmm

我能想到的,应该也是唯一的实现方法就是轮询+忙等待,以下是我的两个猜想

JavaScript的runtime单独开一个不停计时Timer线程,它保持与系统时间同步,每次用户调用setTimeoutsetInerval时,就将任务及其定时信息放到一个列表中,例如:[ timeout任务:时间为123毫秒时执行, interval任务:时间为456毫秒时执行, ... ],然后无限遍历这个任务列表(轮询),将每个任务计划的执行时间与当前系统时间对比,时间到了的就把这个任务的回调函数丢到主线程的执行队列中,并根据任务类型(timeout或interval)删除这个任务或设置任务的下一次执行时间。

第二个猜想我觉得会更合理一些,和第一个的区别就是不单独为Timer开一个线程,而是主线程每趟event loop都执行一次Timer的滴答函数,每次都遍历所有任务,将到时间的任务丢到执行队列中去。相当于这样

(function (global) {
  class Timer {
    constructor() {
      this.schedule = []
      this.timeoutHandler = 1
      this.intervalHandler = 1
      this.tick()
    }

    tick() {
      const now = Date.now()
      let nextSchedule = []
      for(let task of this.schedule) {
        if(task.time >= now) {
          // Timeout!
          if(task.type == 'timeout') {
            setImmediate(task.fn)
          } else if(task.type == 'interval') {
            setImmediate(task.fn)
            task.time += task.interval
            nextSchedule.push(task)  // Schedule next execution
          }
        } else nextSchedule.push(task)  // Not yet
      }
      this.schedule = nextSchedule
      setImmediate(() => this.tick())
    }

    setTimeout(fn, time) {
      this.schedule.push({
        fn,
        type: 'timeout',
        time: Date.now() + time,
        handler: this.timeoutHandler
      })
      return this.timeoutHandler++
    }

    setInterval(fn, interval) {
      this.schedule.push({
        fn,
        interval,
        type: 'interval',
        time: Date.now() + interval,
        handler: this.intervalHandler
      })
      return this.intervalHandler++
    }

    clearTimeout(handler) { ... }
    clearInterval(handler) { ... }
  }

  const timer = new Timer()
  global.setTimeout = function setTimeout(fn, time) { return timer.setTimeout(fn, time) }
  global.setInerval = function setInterval(fn, interval) { return timer.setInterval(fn, interval) }
  ...
})(window)
  • 首先要理解js是单线程执行,在浏览器提供一个js引擎去执行
  • 但为了异步浏览器又提供了几种其他线程,定时器线程就是其中之一,当js代码执行到类似定时器这种异步任务时,把它交给定时器线程去执行,浏览器依然没有阻碍的向下同步执行其代码
  • 定时器线程会对定时器任务进行操作,监听时间,当达到预定时间时,将定时器任务放到一个任务队列
  • 回到js主引擎,js代码继续同步执行,当js执行的这条线程所有任务执行完毕后,去到任务队列取出别的线程放入其中的任务,放到js执行线程中执行,js线程没任务后再去任务队列去取这个过程叫事件循环
  • 所以说定时器预定的时间只是定时器线程监听的时间,时间到了后并不会立即执行定时器的回调函数,而是放到任务队列中,等主线程同步任务执行完毕后才有可能执行,所以说这个时间并不会很准确,受代码复杂执行时间所影响,预定时间只是能执行的最快时间
  • 所以js代码中无论定时器时间再少即使是0,也会放到程序最后执行,当然w3c标准默认最小为20ms

浏览器引擎有JS引擎,毫无疑问专门解析JS代码的,同时浏览器还有属于自己的时间模块,我喜欢这个叫至于你也可以理解为别的方面,而setTimeout与setInterval都隶属于这时间模块,而且最重要的是 定时器属于异步的 也就是只有同步的代码完成后才会执行异步的代码 这就是所谓的线程空余时,事件循环。 所以兄弟,如果非要说底层 希望这个回答对你有所帮助,当然这也是我自己的理解

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题