JS 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毫秒,但它並不能保證兩個函數之間調用的間隔一定是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高級程序設計》中已經闡述過了。簡單來說,如果你的循環:
- 每一次處理不依賴上一次的處理結果;
- 沒有執行的先後順序之分;
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