为何要用 setTimeout 模拟 setInterval ?

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

用多了你就会发现它并非准确无误,极端状况下还会出现一些使人费解的问题。ajax

下面咱们一一罗列..服务器

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

定时器代码:网络

setInterval(fn(), N);

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

因此,在 setInterval 被推入任务队列时,若是在它前面有不少任务或者某个任务等待时间较长好比网络请求等,那么这个定时器的执行时间和咱们预约它执行的时间可能并不一致。spa

好比:.net

let startTime = new Date().getTime();
let count = 0;
//耗时任务
setInterval(function() {
  let i = 0;
  while (i++ < 1000000000);
}, 0);
setInterval(function() {
  count++;
  console.log(
    "与原设定的间隔时差了:",
    new Date().getTime() - (startTime + count * 1000),
    "毫秒"
  );
}, 1000);
// 输出:
// 与原设定的间隔时差了: 699 毫秒
// 与原设定的间隔时差了: 771 毫秒
// 与原设定的间隔时差了: 887 毫秒
// 与原设定的间隔时差了: 981 毫秒
// 与原设定的间隔时差了: 1142 毫秒
// 与原设定的间隔时差了: 1822 毫秒
// 与原设定的间隔时差了: 1891 毫秒
// 与原设定的间隔时差了: 2001 毫秒
// 与原设定的间隔时差了: 2748 毫秒
// ...

能够看出来,相差的时间是愈来愈大的,愈来愈不许确。线程

函数操做耗时过长致使的不许确

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

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

// 作一个网络轮询,每一秒查询一次数据。
let startTime = new Date().getTime();
let count = 0;

setInterval(() => {
    let i = 0;
    while (i++ < 10000000); // 假设的网络延迟
    count++;
    console.log(
        "与原设定的间隔时差了:",
        new Date().getTime() - (startTime + count * 1000),
        "毫秒"
    );
}, 1000)
输出:
// 与原设定的间隔时差了: 567 毫秒
// 与原设定的间隔时差了: 552 毫秒
// 与原设定的间隔时差了: 563 毫秒
// 与原设定的间隔时差了: 554 毫秒(2次)
// 与原设定的间隔时差了: 564 毫秒
// 与原设定的间隔时差了: 602 毫秒
// 与原设定的间隔时差了: 573 毫秒
// 与原设定的间隔时差了: 633 毫秒

setInterval 缺点 与 setTimeout 的不一样

再次强调,定时器指定的时间间隔,表示的是什么时候将定时器的代码添加到消息队列,而不是什么时候执行代码。因此真正什么时候执行代码的时间是不能保证的,取决于什么时候被主线程的事件循环取到,并执行。
setInterval(function, N)
//即:每隔N秒把function事件推到消息队列中

setinterval-1.png

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

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

  • 使用 setInterval 时,某些间隔会被跳过;
  • 可能多个定时器会连续执行;

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

于是咱们通常用 setTimeout 模拟 setInterval,来规避掉上面的缺点。

来看一个经典的例子来讲明他们的不一样:

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

作过的朋友都知道:是一次输出了 5 个 5;
那么问题来了:是每隔 1 秒输出一个 5 ?仍是一秒后当即输出 5 个 5?
答案是:一秒后当即输出 5 个 5
由于 for 循环了五次,因此 setTimeout 被 5 次添加到时间循环中,等待一秒后所有执行。

为何是一秒后输出了 5 个 5 呢?
简单来讲,由于 for 是主线程代码,先执行完了,才轮到执行 setTimeout。

固然为何输出不是 1 到 5,这个涉及到做用域的问题了,这里就不解释了。

setTimeout 模拟 setInterval

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

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

具体实现以下:

1.写一个 interval 方法

let timer = null
interval(func, wait){
    let interv = function(){
        func.call(null);
        timer=setTimeout(interv, wait);
    };
    timer= setTimeout(interv, wait);
 },

2.和 setInterval() 同样使用它

interval(function() {}, 20);

3.终止定时器

if (timer) {
  window.clearSetTimeout(timer);
  timer = null;
}

参考

相关文章
相关标签/搜索