setTimeout 和 setInterval

158 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情

异步编程当然少不了定时器了,常见的定时器函数有 setTimeoutsetIntervalrequestAnimationFrame。最常用的是setTimeout,很多人认为 setTimeout 是延时多久,那就应该是多久后执行。

其实这个观点是错误的,因为 JS 是单线程执行的,如果前面的代码影响了性能,就会导致 setTimeout 不会按期执行。当然了,可以通过代码去修正 setTimeout,从而使定时器相对准确:

 let period = 60 * 1000 * 60 * 2
 let startTime = new Date().getTime()
 let count = 0
 let end = new Date().getTime() + period
 let interval = 1000
 let currentInterval = interval
 function loop() {
   count++
   // 代码执行所消耗的时间
   let offset = new Date().getTime() - (startTime + count * interval);
   let diff = end - new Date().getTime()
   let h = Math.floor(diff / (60 * 1000 * 60))
   let hdiff = diff % (60 * 1000 * 60)
   let m = Math.floor(hdiff / (60 * 1000))
   let mdiff = hdiff % (60 * 1000)
   let s = mdiff / (1000)
   let sCeil = Math.ceil(s)
   let sFloor = Math.floor(s)
   // 得到下一次循环所消耗的时间
   currentInterval = interval - offset 
   console.log('时:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代码执行时间:'+offset, '下次循环间隔'+currentInterval) 
   setTimeout(loop, currentInterval)
 }
 setTimeout(loop, currentInterval)

接下来看 setInterval,其实这个函数作用和 setTimeout 基本一致,只是该函数是每隔一段时间执行一次回调函数。

通常来说不建议使用 setInterval。第一,它和 setTimeout 一样,不能保证在预期的时间执行任务。第二,它存在执行累积的问题,请看以下伪代码

 function demo() {
   setInterval(function(){
     console.log(2)
   },1000)
   sleep(2000)
 }
 demo()

以上代码在浏览器环境中,如果定时器执行过程中出现了耗时操作,多个回调函数会在耗时操作结束以后同时执行,这样可能就会带来性能上的问题。

如果有循环定时器的需求,其实完全可以通过 requestAnimationFrame 来实现:

 function setInterval(callback, interval) {
   let timer
   const now = Date.now
   let startTime = now()
   let endTime = startTime
   const loop = () => {
     timer = window.requestAnimationFrame(loop)
     endTime = now()
     if (endTime - startTime >= interval) {
       startTime = endTime = now()
       callback(timer)
     }
   }
   timer = window.requestAnimationFrame(loop)
   return timer
 }
 let a = 0
 setInterval(timer => {
   console.log(1)
   a++
   if (a === 3) cancelAnimationFrame(timer)
 }, 1000)

首先 requestAnimationFrame 自带函数节流功能,基本可以保证在 16.6 毫秒内只执行一次(不掉帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题,当然你也可以通过该函数来实现 setTimeout

1.1. 为什么使用 setTimeout 实现 setInterval?怎么模拟?

相关知识点:

 // 思路是使用递归函数,不断地去执行 setTimeout 从而达到 setInterval 的效果
 ​
 function mySetInterval(fn, timeout) {
   // 控制器,控制定时器是否继续执行
   var timer = {
     flag: true
   };
 ​
   // 设置递归函数,模拟定时器执行。
   function interval() {
     if (timer.flag) {
       fn();
       setTimeout(interval, timeout);
     }
   }
 ​
   // 启动定时器
   setTimeout(interval, timeout);
 ​
   // 返回控制器
   return timer;
 }

回答:

 setInterval 的作用是每隔一段指定时间执行一个函数,但是这个执行不是真的到了时间立即执行,它真正的作用是每隔一段时间将事件加入事件队列中去,只有当当前的执行栈为空的时候,才能去从事件队列中取出事件执行。所以可能会出现这样的情况,就是当前执行栈执行的时间很长,导致事件队列里边积累多个定时器加入的事件,当执行栈结束的时候,这些事件会依次执行,因此就不能到间隔一段时间执行的效果。
 ​
 针对 setInterval 的这个缺点,我们可以使用 setTimeout 递归调用来模拟 setInterval,这样我们就确保了只有一个事件结束了,我们才会触发下一个定时器事件,这样解决了 setInterval 的问题。

详细资料可以参考: 《用 setTimeout 实现 setInterval》 《setInterval 有什么缺点?》

1.2. js 中倒计时的纠偏实现?

 在前端实现中我们一般通过 setTimeoutsetInterval 方法来实现一个倒计时效果。但是使用这些方法会存在时间偏差的问题,这是由于 js 的程序执行机制造成的,setTimeoutsetInterval 的作用是隔一段时间将回调事件加入到事件队列中,因此事件并不是立即执行的,它会等到当前执行栈为空的时候再取出事件执行,因此事件等待执行的时间就是造成误差的原因。
 ​
 一般解决倒计时中的误差的有这样两种办法:
 ​
 (1)第一种是通过前端定时向服务器发送请求获取最新的时间差,以此来校准倒计时时间。
 ​
 (2)第二种方法是前端根据偏差时间来自动调整间隔时间的方式来实现的。这一种方式首先是以 setTimeout 递归的方式来实现倒计时,然后通过一个变量来记录已经倒计时的秒数。每一次函数调用的时候,首先将变量加一,然后根据这个变量和每次的间隔时间,我们就可以计算出此时无偏差时应该显示的时间。然后将当前的真实时间与这个时间相减,这样我们就可以得到时间的偏差大小,因此我们在设置下一个定时器的间隔大小的时候,我们就从间隔时间中减去这个偏差大小,以此来实现由于程序执行所造成的时间误差的纠正。

详细资料可以参考: 《JavaScript 前端倒计时纠偏实现》