JS setTimeout()與setInterval()的區別

JS setTimeout()setInterval()的區別

  • setTimeout()setInterval()的基本用法我們一帶而過:
  1. 指定延遲後調用函數,
  2. 以指定週期調用函數

setInterval()函數

setInterval(function() {
    func(i++);
}, 100)
  • 每隔100毫秒調用一次func函數,如果func的執行時間少於100毫秒的話,在遇到下一個100毫秒前就能夠執行完:

func的執行時間少於100毫秒

  • 但如果func的執行時間大於100毫秒,該觸發下一個func函數時之前的還沒有執行完怎麼辦?
    答案如下圖所示,那麼第二個func會在隊列(這裏的隊列是指event loop)中等待,直到第一個函數執行完

func的執行時間大於100毫秒

  • 如果第一個函數的執行時間特別長,在執行的過程中本應觸發了許多個func怎麼辦,那麼所有這些應該觸發的函數都會進入隊列嗎?
  • 答案肯定是不會的,只要發現隊列中有一個被執行的函數存在,那麼其他的統統被忽略。如下圖,在第300毫秒和400毫秒處的回調都被拋棄,一旦第一個函數執行完後,接着執行隊列中的第二個,即使這個函數已經“過時”很久了。

隊列中的處理情況

  • 還有一點,雖然在setInterval()函數裏指定的週期是100毫秒,但它並不能保證兩個函數之間調用的間隔一定是100毫秒。在上面的情況中,如果隊列中的第二個函數是在第450毫秒處結束的話,在第500毫秒時,它會繼續執行下一輪func,也就是說這之間的間隔只有50毫秒,而非週期100毫秒。
  • 那如果想保證每次執行的間隔應該怎麼辦?用setTimeout函數。

setTimeout()函數

var i = 1;
var timer = setTimeout(function() {
    alert(i++);
    timer = setTimeout(arguments.callee, 2000);
}, 2000);
  • 上面的函數每2秒鐘遞歸調用自己一次,可以在某一次alert的時候等待任意長的時間(不按"確定"按鈕),接下來無論什麼時候點擊"確定",下一次執行一定離這次確定相差2秒鐘的。
  • 下面上下兩段代碼雖然看上去功能一致,但實際並非如此,原因就是我上面所說
setTimeout(function repeatMe() {
    /* Some long block of code... */
    setTimeout(repeatMe, 10);
}, 10);

setInterval(function() {
    /*Some long block of code...  */
}, 10);

setTimeout()函數除了做定時器外還能幹什麼用?

  • setTimeout()函數當然還有其他非常多的作用。比如說: 在處理DOM點擊事件的時候通常會產生冒泡,正常情況下首先觸發的是子元素的handler,再觸發父元素的handler,如果想讓父元素的handler先於子元素的handler執行應該怎麼辦?那就用setTimeout延遲子元素handler若干個毫秒執行吧。問題是這個"若干個"毫秒應該是多少?可以是0。
  • 你可能會疑惑如果是0的話那不是立即執行了嗎?不,看一下下面的代碼:
(function() {
    setTimeout(function() {
        alert(2);
    }, 0);
    
    alert(1);
})()
  • 先彈出的應該是1,而不是"立即執行"的2。
  • setTimeout()setInterval()都存在一個最小延遲的問題,雖然你給的delay值爲0,但是瀏覽器執行的是自己的最小值。HTML5標準是4ms,但並不意味着所有瀏覽器都會遵循這個標準,包括手機瀏覽器在內,這個最小值既有可能小於4ms也有可能大於4ms。在標準中,如果在setTimeout()中嵌套一個setTimeout(),那麼嵌套的setTimeout()的最小延遲爲10ms。

聊聊setTimeout線程的一些關係

  • 現在我有一個非常耗時的操作(如下面的代碼,在table中插入20000行),我想計算這個操作所消耗的時間應該怎麼辦?你覺得下面這個用new Date來計算的方法怎麼樣:
var t1 = +new Date();

var tbody = document.getElementsByTagName("tbody")[0];
for(var i = 0; i < 20000; i++) {
    var tr = document.createElement("tr");
    for(var t = 0; t < 6; t++) {
        var td = document.createElement("td");
        td.appendChild(document.createTextNode(i + "," + t));
        tr.appendChild(td);
    }
    tbody.appendChild(tr);
}

var t2 = +new Date();
console.log(t2 - t1);
  • 如果你嘗試運行起來就會發現問題,在這20000行還沒有渲染出來的時候,控制檯就已經打印出來了時間,這兩個時間差並非誤差所致(可能這個操作需要5秒,甚至10秒以上),但是打印出來的時間只有1秒左右,這是爲什麼?
  • 因爲Javascript是單線程的(這裏不談web worker),也就是說瀏覽器無論什麼時候都只有一個JS線程在運行JS程序。或許是因爲單線程的緣故,也同時因爲大部分觸發的事件是異步的,JS採用一種隊列(event loop)的機制來處理各個事件,比如用戶的點擊,ajax異步請求,所有的事件都被放入一個隊列中,然後先進先出,逐個執行。這也就解釋了開頭setInterval的那種情況。
  • 另一方面,瀏覽器還有一個GUI渲染線程,當需要重繪頁面時渲染頁面。但問題是GUI渲染線程與JS引擎是互斥的,當JS引擎執行時GUI線程會被掛起,GUI更新會被保存在一個隊列中等到JS引擎空閒時立即被執行。
  • 所以,上面的那個例子中算出的時間只是javascript執行的時間,在這之後,GUI線程纔開始渲染,而此時計時已經結束了。那麼如何你能計算出正確時間了?在結尾添加一個setTimeout()函數。
var t1 = +new Date();

var tbody = document.getElementsByTagName("tbody")[0];
for(var i = 0; i < 20000; i++) {
    var tr = document.createElement("tr");
    for(var t = 0; t < 6; t++) {
        var td = document.createElement("td");
        td.appendChild(document.createTextNode(i + "," + t));
        tr.appendChild(td);
    }
    tbody.appendChild(tr);
}

setTimeout(function () {
    var t2 = +new Date();
    console.log(t2 - t1);
}, 0)
  • 這樣能讓操縱DOM的代碼執行完後不至於立即執行t2 - t1,而在中間空閒的時間恰好允許瀏覽器執行GUI線程。渲染完之後,才計算出時間。

Example

function run() {
    var div = document.getElementsByTagName('div')[0];
    for(var i = 0xA00000; i < 0xFFFFFF; i++) {
        div.style.backgroundColor = '#' + i.toString(16);
    }
}
  • setInterval()函數有一個很重要的應用是javascript中的動畫。
  • 舉個例子,假設我們有一個正方形div,寬度爲100px,現在想讓它的寬度在1000毫秒內增加到300px——很簡單,算出每毫秒內應該增加的像素,再按每毫秒爲週期調用setInterval()實現增長.
var div = $('div')[0];
var width = parseInt(div.style.width, 10);

var MAX = 300, duration = 1000;
var inc = parseFloat((MAX - width)/duration);

function animate(id) {
    width += inc;
    if (width >= MAX) {
        clearInterval(id);
        console.timeEnd("animate");
    }
    div.style.width = width + "px";
}

console.time("animate");
var timer = setInterval(function () {
    animate(timer);
}, 0);
  • 代碼中利用console.time()來計算時間所花費的時間——實際上花的時間是明顯大於1000毫秒的,why? 因爲上面說到最小週期至少應該是4ms,所以每個週期的增長量應該是每毫秒再乘以4。
var inc = parseFloat((MAX - width)/duration)*4;
  • 如果你有心查看Jquery的動畫源碼的話,你能發現源碼的時間週期是13ms,這是我不解的地方——如果追求流暢的動畫效果來說,每秒(1000毫秒)應該是60幀,這樣算下來每幀的時間應該是16.7毫秒,在這裏我把每幀定義爲完成一個像素增量所花的時間,也就是16毫秒(毫秒不允許存在小數)是讓動畫流暢的最佳值。所以Jquery的這個13毫秒值是如何來的了?
  • 無論如何優化setInterval(),誤差是始終存在的。但其實在HTML5中,有一個實踐動畫的最佳途徑requestAnimationFrame。這個函數能保證能以每幀來執行動畫函數。比如上面的例子可以改寫爲:
// init some values
var div = $('div')[0].style;
var height = parseInt(div.height, 10);

// calc distance we need to move per frame over a time
var max = 300;
var steps = (max - height)/seconds/16.7;

// 16.7ms is approx one frame(1000/60)

// loop
function animate(id) {
    height += steps; // use calculated steps
    div.height = height + "px";

    if(height < max) {
        requestAnimationFrame(animate);
    }
}

animate();
  • 關於這個函數和它對應的cancel函數,或者是polyfill就不在這延伸了,如果你有什麼見解關於這部分的可以留言。
  • 這種情況下通常會有多個計時器同時運行,如果同時大量計時器同時運行的話,會引起一些個問題,比如如何回收這些計時器?Jquery的作者John Resig建議建立一個管理中心,它給出的一個非常簡單的代碼如下:
var timers = {
    timerID: 0,
    timers: [],
    add: function(fn) {
        this.timers.push(fn);
    },
    start: function() {
        if (this.timerID) return;
        (function runNext() {
            if (timers.timers.length > 0) {
                for(var i = 0; i < timers.timers.length; i++) {
                    if(timers.timers[i]() === false) {
                        timers.timers.splice(i, 1);
                        i--;
                    }
                }
                timers.timerID = setTimeout(runNext, 0);
            }
        })();
    },
    stop: function() {
        clearTimeout(this.timerID);
        this.timerID = 0;
    }
};
  • 注意看中間的start方法: 他把所有的定時器都存在一個timers隊列(數組)中,只要隊列長度不爲0,就輪詢執行隊列中的每一個子計時器,如果某個子計時器執行完畢(這裏的標誌是返回值是false),那就把這個計時器踢出隊列。繼續輪詢後面的計時器。
  • 上面描述的整個一輪輪詢就是runNext,並且遞歸輪詢,一遍一遍的執行下去timers.timerID = setTimeout(runNext, 0)直到數組爲空。
  • 注意到上面沒有使用到stop方法,Jquery的動畫animate就是使用的是這種機制,不過更完善複雜,摘一段Jquery源碼看看,比如就類似的runNextt這段:
// /src/effects.js:674
jQuery.fx.tick = function() {
    var timer,
        timers = jQuery.timers,
        i = 0;
    
    fxNow = jQuery.now();
    
    for(; i < timers.length; i++) {
        timer = timers[i];
        // Checks the timer has not already been removed
        if(!timer() && timers[i] === timer) {
            timers.splice(i--, 1);
        }
    }

    if(!timers.length) {
        jQuery.fx.stop();
    }
    fxNow = undefined;
};

// /src/effects.js:703
jQuery.fx.start = function() {
    if(!timerId) {
        timerId = setInterval(jQuery.fx.tick, jQuery.fx.interval);
    }
};
  • 不解釋,和上面的那段已經非常類似了,有興趣的親們可以在github上閱讀整段effect.js代碼。
  • 最後setTimeout()函數的應用就是總所周知,來處理因爲js處理時間過長造成瀏覽器假死的問題了。這個技術在《JavaScript高級程序設計》中已經闡述過了。簡單來說,如果你的循環:
  1. 每一次處理不依賴上一次的處理結果;
  2. 沒有執行的先後順序之分;
function chunk(array, process, context) {
    setTimeout(function() {
        var item = array.shift();
        process.call(context, item);
        if(array.length > 0) {
            setTimeout(arguments.callee, 100);
        }
    }, 100);
}
  • chunk()函數的用途就是將一個數組分成小塊處理,它接受三個參數: 要處理的數組,處理函數以及可選的上下文環境。每次函數都會將數組中第一個對象取出交給process()函數處理,如果數組中還有對象沒有被處理則啓動下一個timer,直到數組處理完。這樣可保證腳本不會長時間佔用處理機,使瀏覽器出一個高響應的流暢狀態。

JackDan9 Thinking

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章