讀古今文學網 > 編寫高質量代碼:改善JavaScript程序的188個建議 > 建議119:使用定時器優化UI隊列 >

建議119:使用定時器優化UI隊列

在JavaScript中使用setTimeout或setInterval創建定時器時,這兩個函數都接收一樣的參數:一個是要執行的函數,另一個是執行這個函數之前的等待時間(單位毫秒)。setTimeout函數創建一個只運行一次的定時器,而setInterval函數創建一個週期性重複運行的定時器。

定時器與UI線程交互的方式有助於分解長運行腳本為較短的片斷。調用setTimeout或setInterval告訴JavaScript引擎等待一定時間,然後將JavaScript任務添加到UI隊列中。例如:


function greeting{

alert(\"Hello world!\");

}

setTimeout(greeting,250);


在上面代碼中,在250 ms之後向UI隊列插入一個JavaScript任務來運行greeting函數。在此時間點之前,所有其他UI更新和JavaScript任務都在運行。記住,第二個參數指出什麼時候應當將任務添加到UI隊列之中,並不是說那時代碼將被執行,這個任務必須等到隊列中的其他任務都執行之後才能被執行。例如:


var button=document.getElementById(\"my-button\");

button.onclick=function{

oneMethod;

setTimeout(function{

document.getElementById(\"notice\").style.color=\"red\";

},250);

};


在上面示例中,當按鈕被單擊時,將調用一個方法設置一個定時器。用於修改notice元素顏色的代碼被包含在一個定時器設備中,它將在250 ms之後被添加到隊列中。250 ms是從調用setTimeout時開始計算的,而不是從整個函數運行結束時開始計算的。如果setTimeout在時間點n上被調用,那麼運行定時器代碼的JavaScript任務將在n+250的時刻加入UI隊列。

定時器代碼只有等創建它的函數運行完成之後才有可能被執行。假設在前面的代碼中定時器延時變得更小,在創建定時器之後又調用了另一個函數,那麼定時器代碼有可能在onclick事件處理完成之前加入隊列。


var button=document.getElementById(\"my-button\");

button.onclick=function{

oneMethod;

setTimeout(function{

document.getElementById(\"notice\").style.color=\"red\";

},50);

anotherMethod;

};


如果anotherMethod執行時間超過50 ms,那麼定時器代碼將在onclick處理完成之前加入到隊列中。其結果是等onclick處理運行完畢,定時器代碼立即執行,察覺不出其間的延遲。

在任何一種情況下,創建一個定時器會造成UI線程暫停,如同定時器會從一個任務切換到下一個任務。因此,定時器代碼復位所有相關的瀏覽器限制,包括長運行腳本時間。此外,調用棧也在定時器代碼中復位為零。這一特性使定時器成為長運行JavaScript代碼理想的跨瀏覽器解決方案。

JavaScript定時器延時往往不準確,快慢大約幾毫秒。指定定時器延時250 ms,並不意味任務將在調用setTimeout之後精確的250 ms後加入隊列。所有瀏覽器試圖盡可能準確,但通常會發生幾毫秒的滑移,或快或慢。正因為這個原因,定時器不可用於測量實際時間。

在Windows系統上定時器的分辨率為15 ms,也就是說,一個值為15的定時器延時將根據最後一次系統時間的刷新而轉換為0或15。由於設置定時器延時小於15將在IE中導致瀏覽器鎖定,所以建議最小值為25 ms(實際時間是15 ms或30 ms),以確保至少15 ms的延遲。

最小定時器延時也有助於避免其他瀏覽器和操作系統上產生的定時器分辨率問題。大多數瀏覽器在定時器延時小於10 ms時表現出差異性。

一個常見的長運行腳本就是循環佔用了太長的運行時間。如果嘗試循環優化之後還不能縮減足夠的運行時間,那麼定時器就是下一個優化步驟。基本方法是將循環工作分解到定時器序列中。典型的循環模式如下:


for(var i=0,len=items.length;i<len;i++){

process(items[i]);

}


導致循環結構運行時間過長的因素有兩個:process的複雜度和items的大小。這兩個因素有可能同時存在。可用定時器取代循環的兩個決定性因素如下:

❑處理過程不需要同步處理。

❑數據不需要按順序處理。

一種基本異步代碼模式如下:


var todo=items.concat;

setTimeout(function{

process(todo.shift);

if(todo.length>0){

setTimeout(arguments.callee,25);

}else{

callback(items);

}

},25);


這個模式的基本思想是創建一個原始數組的副本,將它作為處理對象。第一次調用setTimeout創建一個定時器處理隊列中的第一個項。調用todo.shift返回它的第一個項,然後將它從數組中刪除。第一項的值作為參數傳給process。接著檢查是否還有更多項需要處理。如果todo隊列中還有內容,那麼就再啟動一個定時器。因為下個定時器需要運行相同的代碼,所以將第一個參數傳入arguments.callee,此值指向當前正在運行的匿名函數。如果不再有內容需要處理,那麼將調用callback函數。此模式與循環相比需要更多代碼,可將此功能封裝起來,例如:


function processArray(items,process,callback){

var todo=items.concat;

setTimeout(function{

process(todo.shift);

if(todo.length>0){

setTimeout(arguments.callee,25);

}else{

callback(items);

}

},25);

}


processArray函數以一種可重用的方式實現了先前的模板,並且接收3個參數:待處理數組、對每個項調用的處理函數、處理結束時執行的回調函數。該函數用法如下:


var items=[123,789,323,778,232,654,219,543,321,160];

function outputValue(value){

console.log(value);

}

processArray(items,outputValue,function{

console.log(\"Done!\");

});


此段代碼使用processArray方法將數組值輸出到終端,當所有處理結束時再打印一條消息。通過將代碼封裝在一個函數中,定時器可在多處重用而無須多次實現。