JavaScript BOM -- setTimeout()和setInterval() 常見問題
定義
setTimeout()和setInterval()經常被用來處理延時和定時任務。setTimeout() 方法用於在指定的毫秒數後呼叫函式或計算表示式,而setInterval()則可以在每隔指定的毫秒數迴圈呼叫函式或表示式,直到clearInterval把它清除。
window.setTimeout(code,millisec);
window.setInterval(code,millisec)
從定義上我們可以看到兩個函式十分類似,只不過前者執行一次,而後者可以執行多次,兩個函式的引數也相同,第一個引數是要執行的code或控制代碼,第二個是延遲的毫秒數。
很簡單的定義,使用起來也很簡單,但有時候我們的程式碼並不是按照我們的想象精確時間被呼叫的,很讓人困惑,第二個引數表示等待多長時間的毫秒數,但經過該事件後指定的程式碼 不一定會執行 。
第一個問題:為什麼不能精確執行
看個簡單的例子,簡單頁面在載入完兩秒後,寫下Delayed alert!
setTimeout('document.write("Delayed alert!");', 2000);
看起來很合理,我們再看個setInterVal()方法的例子
var num = 0; var i = setInterval(function() { num++; var date = new Date(); document.write(date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds() + '<br>'); if (num > 10) clearInterval(i); }, 1000);
頁面每隔1秒記錄一次當前時間(分鐘:秒:毫秒),記錄十次後清除,不再記錄。考慮到程式碼執行時間可能記錄的不是執行時間,但時間間隔應該是一樣的,看看結果
43:39:130 43:40:144 43:41:158 43:42:172 43:43:186 43:44:200 43:45:214 43:46:228 43:47:242 43:48:256
為什麼
時間間隔幾乎是1000毫秒,但不精確,這是為什麼呢?原因在於我們對JavaScript定時器存在一個誤解,JavaScript其實是執行在單執行緒的環境中的,一定時間內只能執行一段程式碼,為了控制要執行的程式碼就有一個JS任務佇列,這些任務會按照將他們新增到佇列的順序執行。setTimeout()的第二個引數告訴JS再過多長時間把當前任務新增到佇列中。若佇列為空,這段新增的程式碼會立即執行,若不為空,就要等前面的程式碼執行完再執行。
這就意味著定時器僅僅是計劃程式碼在未來的某個時間執行,而具體執行時機是不能保證的,因為頁面的生命週期中,不同時間可能有其他程式碼在控制JavaScript程序,能否按計劃執行,完全看所謂的任務佇列是否在指定時間到達時是否為空。在頁面下載完成後程式碼的執行、事件處理程式、Ajax回撥函式都是使用同樣的執行緒,實際上瀏覽器負責進行排序,指派某段程式在某個時間點執行的優先順序。(參看我的另一篇文章 javascript Core -- 執行機制)
我們把效果放大一下看看,新增一個耗時的任務
function test() {
for (var i = 0; i < 500000; i++) {
var div = document.createElement('div');
div.setAttribute('id', 'testDiv');
document.body.appendChild(div);
document.body.removeChild(div);
}
}
setInterval(test, 10);
var num = 0;
var i = setInterval(function() {
num++;
var date = new Date();
document.write(date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds() + '<br>');
if (num > 10)
clearInterval(i);
}, 1000);
我們又加入了一個定時任務,看看結果
47:9:222
47:12:482
47:16:8
47:19:143
47:22:631
47:25:888
47:28:712
47:32:381
47:34:146
47:35:565
47:37:406
這下效果明顯了,差距甚至都超過了3秒,而且差距很不一致。
正如上文所言,除了主JavaScript程序執行棧外,還需要一個在程序下一次空閒時執行的任務佇列。隨著頁面生命週期推移,程式碼會按照執行順序新增入佇列,例如當按鈕被按下的時候他的事件處理程式會被新增到佇列中,並在下一個可能時間內執行。在接到某個Ajax響應時,回撥函式的程式碼會被新增到佇列。JavaScript中沒有任何程式碼是立即執行的,但一旦主程序空閒則儘快執行。定時器對佇列的工作方式是當特定時間過去後將程式碼插入,這並不意味著它會馬上執行,只能表示它儘快執行。
知道了這些後,我們就能明白,如果想要精確的時間控制,是不能依賴於JavaScript的setTimeout函式的。
第二條:使用setTimeout()模擬setInterval()
使用 setInterval() 建立的定時器可以使程式碼迴圈執行,到有指定效果的時候,清除interval就可以,如下例
var my_interval = setInterval(function () {
if (condition) {
//..........
} else {
clearInterval(my_interval);
}
}, 100);
但這個方式的問題在於定時器的程式碼可能在程式碼再次被新增到佇列之前還沒有執行完成,結果導致迴圈內的判斷條件不準確,程式碼多執行幾次,之間沒有停頓。不過JavaScript已經解決這個問題,當使用setInterval()時,僅當沒有該定時器的其他程式碼例項時才將定時器程式碼插入佇列。這樣確保了定時器程式碼加入到佇列的最小時間間隔為指定間隔。
這樣的規則帶來兩個問題
1. 某些間隔會被跳過 2.多個定時器的程式碼執行之間的間隔可能比預期要小
為了避免這兩個缺點,我們可以使用setTimeout()來實現重複的定時器
setTimeout(function () {
//code
setTimeout(arguments.callee, interval);
}, interval)
這樣每次函式執行的時候都會建立一個新的定時器,第二個setTimeout()呼叫使用了agrument.callee 來獲取當前實行函式的引用,並設定另外一個新定時器。這樣做可以保證在程式碼執行完成前不會有新的定時器插入,並且下一次定時器程式碼執行之前至少要間隔指定時間,避免連續執行。
setTimeout(function () {
var div = document.getElementById('moveDiv');
var left = parseInt(div.style.left) + 5;
div.style.left = left + 'px';
if (left < 200) {
setTimeout(arguments.callee, 50);
}
}, 50);
這段定時器程式碼每次執行的時候,把一個div向右移動5px,當座標大於200的時候停止。
第三條:JavaScript中setInterval傳參常見的問題(setInterval第一個引數加引號與不加引號區別)
function fun() {
console.log("1");
}
setInterval("fun()",1000);//全域性作用域下正常執行
setInterval(fun(),1000); //呼叫函式正常,setInterval調用出錯
setInterval(fun,1000); //正確
setInterval(function(){ //匿名函式呼叫
console.log("1");
})
如例子中所示,setInterval 第一個引數 可以是函式名、匿名函式、函式的引用以及其他可執行程式碼。
setInterval("fun()",1000)
這種加引號的方式就可以理解為 可執行程式碼 就行eval() 一樣去執行第一個引數,就是對fun方法的呼叫 理所當然的彈出 1 一秒鐘間隔,一直執行。
setInterval(fun(),1000)
fun() 是對函式的直接呼叫,也就是說當setInterval還沒有開始函式fun就執行了。如果這個函式沒有返回值或者返回值不是可執行的函式或者其他的程式碼的話,就以上程式碼而言只是彈出一個1之後就停止了,這種方法無意義。但是這種方法也是可以間隔執行的,例如改造成如下
function funone() {
return function () {
alert("qishiwoyenengzhixing")
}
}
setInterval(funone(), 1000);//你敢說我不能執行?
setInterval(fun,1000)
此時setInterval的第一個引數fun看作引數為 函式名或函式的引用。
<span style="white-space:pre"> </span>setInterval(function () {
alert("我一秒中執行一次");
}, 1000)
PS:
setTimeout( console.log(1) , 1000);
setTimeout( console.log(2) , 800);
setTimeout( console.log(3) , 600);
setTimeout( 'console.log(1)' , 1000);
setTimeout( 'console.log(2)' , 800);
setTimeout( 'console.log(3) ', 600);
結合第一問和第三問,你能回答出這兩段程式碼之間的區別嗎?