定時器不許時☞帶你揭祕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
}
複製代碼
相關文章
相關標籤/搜索