定时器不许时☞带你揭秘setTimeout和setInterval

1、一个面试题引发的思考

某天上班摸鱼,一个Q群里有人在发笔试题在线求助。大概瞄了一下。发现里面有道主观判断题。面试

代码中有setInterval(()=>{console.log('a')},10000),那必定会每隔10秒在控制台打印个a。浏览器

可能不少人第一印象,包括我再内,都认为这道题是对的。可是实际上是错的!!bash

为何呢,就是JavaScript执行机制搞得鬼,那什么是JavaScript执行机制,不懂能够点这里看一下。app

2、setTimeout的定义和用法

一、setTimeout的定义

setTimeout()方法用于在指定的毫秒数后调用函数或计算表达式。less

二、setTimeout的参数

  • 第一个参数function,必填的,回调函数,能够是一个函数,也能够是一个函数名。函数

  • 第二个参数delay,可选的,延迟时间,单位是ms。post

  • 第三个参数param1,param2,param3...,可选的,是传递给回调函数的参数,比较不经常使用到,在IE9 及其更早版本不支持该参数。优化

    setTimeout(function(a) {
    	console.log(a);
    }, 2000,'我是定时器')
    复制代码
    setTimeout(foo, 2000,'我是定时器')
    function foo(a){
        console.log(a)
    }
    复制代码

三、setTimeout的返回值

返回一个 ID(数字),能够将这个ID传递给clearTimeout()来取消执行。动画

3、setInterval的定义和用法

一、setInterval的定义

setInterval()方法可按照指定的周期(以毫秒计)来调用函数或计算表达式。ui

二、setInterval的参数

  • 第一个参数function,必填的,回调函数,能够是一个函数,也能够是一个函数名。
  • 第二个参数delay,可选的,间隔时间,单位是ms。
  • 第三个参数param1,param2,param3...,可选的,是传递给回调函数的参数,比较不经常使用到,在IE9 及其更早版本不支持该参数。
    setInterval(function(a) {
    	console.log(a);
    }, 2000,'我是定时器')
    复制代码
    setInterval(foo, 2000,'我是定时器')
    function foo(a){
        console.log(a)
    }
    复制代码

三、setInterval的返回值

返回一个 ID(数字),能够将这个ID传递给clearInterval()以取消执行。

4、setTimeout的最短延迟时间

第二个参数delay未设置的时候,默认为0,意味着“立刻”执行,或者尽快执行。

可是有一个规定以下

If timeout is less than 0, then set timeout to 0. If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.

上面的意思是说,若是延迟时间短于0,则将延迟时间设置为0。若是嵌套级别大于5,延迟时间短于4ms,则将延迟时间设置为4ms。

还有另一种状况。为了节电,对于那些不处于当前窗口的页面,浏览器会将最短延时限制扩大到1000ms。

以上能够说是形成定时器不许时缘由之一

5、setInterval的最短间隔时间

在John Resig的新书《Javascript忍者的秘密》一书中提到

Browsers all have a 10ms minimum delay on OSX and a(approximately) 15ms delay on Windows.

在苹果机上的最短间隔时间是10毫秒,在Windows系统上的最短间隔时间大约是15毫秒。

大多数电脑显示器的刷新频率是60HZ,大概至关于每秒钟重绘60次。所以,最平滑的动画效的最佳循环间隔是1000ms/60,约等于16.6ms。

综上所述,我认为setInterval的最短间隔时间应该为16.6ms。

6、不许时的setTimeout和setInterval

不论是哪一种状况,实际的延迟时间可能会比期待的(delay毫秒数) 值长。

除了设置的delay比最短延迟时间和最短间隔时间还短形成的,还有一个缘由就是JavaScript执行机制形成,下面以一个例子来分析。

<body>
    <button id="btn"></button>
    <script>
        const btn = document.getElementById("btn");
        btn.addEventListener('click',function handleClick(){
            //...代码执行时间需80ms
        })
    	setTimeout(function handlerTimeout(){
            //...代码执行时间需60ms
        }, 100);
        setInterval(function handlerInterval(){
            //...代码执行时间需80ms
        },100)
        //... 其他代码执行时间须要180ms
    </script>
</body>
复制代码

咱们借助一个时间轴来描述这段代码是怎么执行的。

在100ms时,原本两个定时器是同时完成的,可是setTimeout定时器写在前面,因此其回调函数handlerTimeout先进入事件队列先执行。回调函数handlerInterval后进入事件队列后执行。

可是实际状况是,由于还有其他代码执行时间须要180ms,也就是说主线程中须要到180ms时才有空闲,因此回调函数handlerTimeout只能180ms时才能执行。回调函数handlerInterval须要等回调函数handlerTimeout执行完才能执行,至关在240ms时才执行。

为何会出现上述现象,真正的缘由能够去看一下个人另外一篇文章JavaScript究竟是怎么执行的🔥

因此能够得出一个结论setTimeout、setInterval没法保证准时执行回调函数。

7、被废弃的setInterval回调函数

在200ms时,setInterval又执行完了,回调函数handlerInterval会不会又进入事件队列。

答案是不会,由于此时事件队列中已经有一个回调函数handlerInterval了。

此时setInterval回调函数是被废弃了。

8、setInterval回调函数的执行时间

在240ms时,回调函数handlerTimeout执行结束,开始执行回调函数handlerInterval。

在300ms时,setInterval又执行完了,发现事件队列中已经没有回调函数handlerInterval了。这时回调函数handlerInterval会进入事件队列。

在320ms时,上个回调函数handlerTimeout执行结束,下个回调函数handlerTimeout接着立刻执行。

在400ms时,setInterval又执行完了,发现事件队列中已经没有回调函数handlerInterval了。这时回调函数handlerInterval会进入事件队列。刚好上个回调函数handlerTimeout执行结束,下个回调函数handlerTimeout接着立刻执行。

这时就会发现,回调函数handlerTimeout执行起来没间隔,间隔不见了。

因此setInterval的间隔时间必定要比回调函数的执行时间大。

可是在不少状况下,咱们并不能清晰的知道回调函数的执行时间,为了能按照必定的间隔周期性的触发定时器,能够用如下方法实现。

setTimeout(function handlerInterval(){
    // do something
    setTimeout(handlerInterval,100); 
    // 执行完处理程序的内容后,在末尾再间隔100毫秒来调用该程序,这样就能保证必定是100毫秒的周期调用
},100)
复制代码

可是这个方法有个时间偏差,在优化一下

function mySetInterval(timeout) {
    const startTime = new Date().getTime();
    let countIndex = 1;
    let onOff=true;
    startSetInterval(timeout)
    function startSetInterval(interval) {
        setTimeout(() => {
            const endTime = new Date().getTime();
            // 误差值
            const deviation = endTime - (startTime + countIndex * timeout);
            console.log(`${countIndex}: 误差${deviation}ms`);
            countIndex++;
            // 下一次
            if(onOff){
                startSetInterval(timeout - deviation);
            }
        }, interval);
    }
    
    function stopSetInterval(){
        onOff=false;
    }
    return stopSetInterval
}
复制代码
相关文章
相关标签/搜索