原文:
http://qingbob.com/difference-between-settimeout-setinterval/
setTimeout和setInterval的基本用法我們一帶而過:
- 指定延遲后調用函數,
- 以指定周期調用函數
讓我們想象一個意外情況,比如說下面的setInterval
setInterval(function () { func(i++); }, 100)
我們每100毫秒調用一次func函數,如果func的執行時間少於100毫秒的話,在遇到下一個100毫秒前就能夠執行完:
;
但如果func的執行時間大於100毫秒,該觸發下一個func函數時之前的還沒有執行完怎么辦?答案如下圖所示,那么第二個func會在隊列(這里的隊列是指event loop)中等待,直到第一個函數執行完
;
如果第一個函數的執行時間特別長,在執行的過程中本應觸發了許多個func怎么辦,那么所有這些應該觸發的函數都會進入隊列嗎?
不,只要發現隊列中有一個被執行的函數存在,那么其他的統統忽略。【文中的說法的確有誤,是不會被忽略。而是繼續加入到隊列中。(看原文的評論)】如下圖,在第300毫秒和400毫秒處的回調都被拋棄,一旦第一個函數執行完后,接着執行隊列中的第二個,即使這個函數已經“過時”很久了。
;
還有一點,雖然你在setInterval的里指定的周期是100毫秒,但它並不能保證兩個函數之間調用的間隔一定是一百毫秒。在上面的情況中,如果隊列中的第二個函數時在第450毫秒處結束的話,在第500毫秒時,它會繼續執行下一輪func,也就是說這之間的間隔只有50毫秒,而非周期100毫秒
那如果我想保證每次執行的間隔應該怎么辦?用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除了做定時器外還能干什么用?
非常多,比如說:在處理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中插入2000行),我想計算這個操作所耗的時間應該怎么辦?你覺得下面這個用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);
如果你嘗試運行起來就會發現問題,在這2000行還沒有渲染出來的時候,控制台就已經打印出來了時間,這兩個時間差並非誤差所致(可能這個操作需要5秒,甚至10秒以上),但是打印出來的時間只有1秒左右,這是為什么?
因為Javascript是單線程的(這里不談web worker),也就是說瀏覽器無論什么時候都只有一個JS線程在運行JS程序。或許是因為單線程的緣故,也同時因為大部分觸發的事件是異步的,JS采用一種隊列(event loop)的機制來處理各個事件,比如用戶的點擊,ajax異步請求,所有的事件都被放入一個隊列中,然后先進先出,逐個執行。這也就解釋了開頭setInterval的那種情況。
另一方面,瀏覽器還有一個GUI渲染線程,當需要重繪頁面時渲染頁面。但問題是GUI渲染線程與JS引擎是互斥的,當JS引擎執行時GUI線程會被掛起,GUI更新會被保存在一個隊列中等到JS引擎空閑時立即被執行。
所以,在腳本中執行對界面進行更新操作,如添加結點,刪除結點或改變結點的外觀等更新並不會立即體現出來,這些操作將保存在一個隊列中,待JavaScript引擎空閑時才有機會渲染出來.
所以,上面的那個例子中算出的時間只是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線程。渲染完之后,才計算出時間。
下面這個例子也是同樣的道理,可以如何改進才能看到顏色的改變呢?留作作業吧:
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毫秒的,為什么?因為上面說到最小周期至少應該是4ms,所以每個周期的增長量應該是沒每毫秒再乘以四
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); var seconds = 1; //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源碼看看,比如就類似的runNext這段:
// /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高級程序設計》中已經闡述過了(沒有誰沒有讀過這本書吧)。簡單來說,如果你的循環
- 每一次處理不依賴上一次的處理結果;
- 沒有執行的先后順序之分; 3.(呃,忘了)。
因為手頭上暫時找不到這本書,在網上找了一段類似的代碼作為拋磚引玉作為結尾吧,有興趣的同學可以去回顧這段:
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,直到數組處理完。這樣可保證腳本不會長時間占用處理機,使瀏覽器出一個高響應的流暢狀態。
更新於2014.3.18:
有幾位朋友詢問我上面那道題的正確答案。首先要道一個歉,是因為瀏覽器運行那段代碼其實已經很吃力了,如果運行完整的答案代碼,瀏覽器一定會崩潰。但原理是一致的,為了能夠保證瀏覽器能運行,能夠變換顏色,我把最終顏色FFFFFF
,改為BBBBBB
。答案如下:
(function run() { var div = document.getElementsByTagName('div')[0] for(var i=0xA00000;i<0xBBBBBB;i++) { (function (color) { setTimeout(function () { div.style.backgroundColor = '#' + color.toString(16); }); })(i); } })()
參考資料: