为什么要用 setTimeout 模拟 setInterval ?

在JS 事件循环之宏任务和微任务中讲到过,setInterval 是一个宏任务。

用多了你就会发现它并不是准确无误,极端情况下还会出现一些令人费解的问题。

下面我们一一罗列..

推入任务队列后的时间不准确

定时器代码:

1setInterval(fn(), N); 2

上面这句代码的意思其实是fn()将会在 N 秒之后被推入任务队列

所以,在 setInterval 被推入任务队列时,如果在它前面有很多任务或者某个任务等待时间较长比如网络请求等,那么这个定时器的执行时间和我们预定它执行的时间可能并不一致。

比如:

1let startTime = new Date().getTime(); 2let count = 0; 3//耗时任务 4setInterval(function() { 5  let i = 0; 6  while (i++ < 1000000000); 7}, 0); 8setInterval(function() { 9  count++; 10  console.log( 11    "与原设定的间隔时差了:", 12    new Date().getTime() - (startTime + count * 1000), 13    "毫秒" 14  ); 15}, 1000); 16// 输出: 17// 与原设定的间隔时差了:699 毫秒 18// 与原设定的间隔时差了:771 毫秒 19// 与原设定的间隔时差了:887 毫秒 20// 与原设定的间隔时差了:981 毫秒 21// 与原设定的间隔时差了:1142 毫秒 22// 与原设定的间隔时差了:1822 毫秒 23// 与原设定的间隔时差了:1891 毫秒 24// 与原设定的间隔时差了:2001 毫秒 25// 与原设定的间隔时差了:2748 毫秒 26// ... 27

可以看出来,相差的时间是越来越大的,越来越不准确。

函数操作耗时过长导致的不准确

考虑极端情况,假如定时器里面的代码需要进行大量的计算(耗费时间较长),或者是 DOM 操作。这样一来,花的时间就比较长,有可能前一次代码还没有执行完,后一次代码就被添加到队列了。也会到时定时器变得不准确,甚至出现同一时间执行两次的情况。

最常见的出现的就是,当我们需要使用 ajax 轮询服务器是否有新数据时,必定会有一些人会使用 setInterval ,然而无论网络状况如何,它都会去一遍又一遍的发送请求,最后的间隔时间可能和原定的时间有很大的出入。

1// 做一个网络轮询,每一秒查询一次数据。 2let startTime = new Date().getTime(); 3let count = 0; 4 5setInterval(() => { 6    let i = 0; 7    while (i++ < 10000000); // 假设的网络延迟 8    count++; 9    console.log( 10        "与原设定的间隔时差了:", 11        new Date().getTime() - (startTime + count * 1000), 12        "毫秒" 13    ); 14}, 1000) 15输出: 16// 与原设定的间隔时差了:567 毫秒 17// 与原设定的间隔时差了:552 毫秒 18// 与原设定的间隔时差了:563 毫秒 19// 与原设定的间隔时差了:554 毫秒(2次) 20// 与原设定的间隔时差了:564 毫秒 21// 与原设定的间隔时差了:602 毫秒 22// 与原设定的间隔时差了:573 毫秒 23// 与原设定的间隔时差了:633 毫秒 24

setInterval 缺点 与 setTimeout 的不同

再次强调,定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行。

1setInterval(function, N) 2//即:每隔N秒把function事件推到消息队列中 3

setinterval-1.png

上图可见,setInterval 每隔 100ms 往队列中添加一个事件;100ms 后,添加 T1 定时器代码至队列中,主线程中还有任务在执行,所以等待,some event 执行结束后执行 T1 定时器代码;又过了 100msT2 定时器被添加到队列中,主线程还在执行 T1 代码,所以等待;又过了 100ms ,理论上又要往队列里推一个定时器代码,但由于此时 T2 还在队列中,所以T3 不会被添加(T3 被跳过),结果就是此时被跳过;这里我们可以看到,T1 定时器执行结束后马上执行了 T2 代码,所以并没有达到定时器的效果。

综上所述,setInterval 有两个缺点:

  • 使用 setInterval 时,某些间隔会被跳过;

  • 可能多个定时器会连续执行;

可以这么理解:每个 setTimeout 产生的任务会直接 push 到任务队列中;而 setInterval 在每次把任务 push 到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中,如果有则不添加,没有则添加)。

因而我们一般用 setTimeout 模拟 setInterval ,来规避掉上面的缺点。

来看一个经典的例子来说明他们的不同:

1for (var i = 0; i < 5; i++) { 2  setTimeout(function() { 3    console.log(i); 4  }, 1000); 5} 6

做过的朋友都知道:是一次输出了 55 ; 那么问题来了:是每隔 1 秒输出一个 5 ?还是一秒后立即输出 55 ?答案是:一秒后立即输出 55因为 for 循环了五次,所以 setTimeout5 次添加到时间循环中,等待一秒后全部执行。

为什么是一秒后输出了 **5 个 5 呢?**简单来说,因为 for 是主线程代码,先执行完了,才轮到执行 setTimeout

当然为什么输出不是 15 ,这个涉及到作用域的问题了,这里就不解释了。

setTimeout 模拟 setInterval

综上所述,在某些情况下,setInterval 缺点是很明显的,为了解决这些弊端,可以使用 setTimeout() 代替。

  • 在前一个定时器执行完前,不会向队列插入新的定时器(解决缺点一)

  • 保证定时器间隔(解决缺点二)

具体实现如下:

1.写一个 interval 方法

1let timer = null 2interval(func, wait){ 3    let interv = function(){ 4        func.call(null); 5        timer=setTimeout(interv, wait); 6    }; 7    timer= setTimeout(interv, wait); 8 }, 9

2.和 setInterval() 一样使用它

1interval(function() {}, 20); 2

3.终止定时器

1if (timer) { 2  window.clearSetTimeout(timer); 3  timer = null; 4} 5

参考

  • 为什么要用 setTimeout 模拟 setInterval ?

  • 用 settTimeout()代替 setInterval()

来自:SegmentFault,作者:九旬

链接:https://segmentfault.com/a/1190000038829248

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

JavaScript 和 Node.js 中事件循环

1.JavaScript中事件循环可以参考《JavaScript忍者秘籍第二版》第十三章,讲解的很好。JavaScript中事件循环,主要就在理解宏任务和微任务这两种异步任务。宏任务(macrotask):setTimeOut、setInterval、setImmediate、I/O、各种callback、UI渲染、messageCh