讀古今文學網 > 編寫高質量代碼:改善JavaScript程序的188個建議 > 建議181:使用setTimeout實現工作線程 >

建議181:使用setTimeout實現工作線程

在Ajax應用中,有時候需要在後台執行一些耗時較長且與頁面主要邏輯無關的操作。例如,對於一個在線文檔編輯器來說,會需要定期自動備份用戶當前所編輯的內容,這樣當應用異常崩潰時,用戶還能恢復所編輯的內容。這樣的定期備份任務可能需要花費一些時間,但優先級較低。類似這樣的任務還有頁面內容的預先加載和日誌記錄等。對於這些任務,最好的實現方式是在後台工作線程中執行它們,這樣不會對用戶在主頁面上的操作造成影響。用戶並不會希望由於後台備份正在進行,而無法對當前的文檔進行編輯。

瀏覽器中JavaScript引擎是單線程執行的,也就是說,在同一時間內,只能有一段代碼被JavaScript引擎執行。如果在同一時間內還有其他代碼需要執行,那麼這些代碼需要等待JavaScript引擎執行完當前的代碼之後才有可能獲得被執行的機會。在正常情況下,作為頁面加載過程中的重要一步,JavaScript引擎會順序執行頁面上的所有JavaScript代碼。在頁面加載完成之後,JavaScript引擎會進入空閒狀態。用戶在頁面上的操作會觸發一些事件,這些事件的處理方法會交給JavaScript引擎來執行。鑒於JavaScript引擎的單線程特性,一般會在內部維護一個待處理的事件隊列,每次從事件隊列中選出一個事件處理方法來執行。如果在執行過程中,有新的事件發生,那麼新事件的處理方法只會被加入到隊列中等待執行。如果當前正在執行的事件處理方法非常耗時,那麼隊列中的其他事件處理方法可能長時間無法得到執行,造成用戶界面失去響應,嚴重影響用戶的使用體驗。

JavaScript引擎的這種工作方式類似於早期的單核CPU的調度方式。單核CPU雖然也支持多任務同時運行,但實際上同一時間只能有一個任務在執行。CPU通過時間片的輪轉來保證每個任務都有一定的執行時間。JavaScript並沒有原生提供與操作系統中的線程類似的結構,但可以通過定時器機制來模擬。JavaScript提供了兩個基本的方法來執行與定時相關的操作,分別是setTimeout和setInterval。

❑setTimeout用來設置在指定的間隔時間之後執行某個JavaScript方法。setTimeout的方法聲明非常簡單:setTimout(func,time),其中參數func表示的是要執行的JavaScript方法,可以是JavaScript方法對像或方法體的字符串;參數time表示的是以毫秒為單位的間隔時間。

❑setInterval用來設置根據指定的間隔重複執行某個JavaScript方法。setInterval的方法聲明與setTimeout相同:setInterval(func,time),這裡參數time指定的是方法func重複執行的間隔。當setTimeout或setInterval被調用的時候,瀏覽器會根據設置的時間間隔來觸發相應的事件。

如果代碼的調用方式是setTimeout(func,100),那麼該代碼被執行100 ms後,定時器的事件被觸發。如果這個時候JavaScript引擎中沒有正在執行的其他代碼,那麼與此定時器對應的JavaScript方法func就可以被執行,否則,該JavaScript方法的執行就被加入到等待的隊列中。當JavaScript引擎空閒的時候,會從這個隊列中選擇一個等待的JavaScript方法來執行。也就是說,雖然在調用setTimeout時設置的間隔時間是100 ms,但與之對應的JavaScript方法實際被執行的間隔有可能大於設定的100 ms,這取決於是否有其他代碼正在被執行和執行所花費的時間。因此,setTimeout實際生效的間隔時間可能大於設定的時間。

setInterval的執行方式與setTimeout有很大不同。如果代碼的調用方式是setInterval(func,100),那麼每隔100 ms,定時器的事件就會被觸發。與setTimeout相同的是,如果當前JavaScript引擎空閒,那麼定時器對應的方法func會被立即執行,否則,該JavaScript方法的執行就會被加入到等待隊列中。由於定時器的事件是每隔100 ms就觸發一次,有可能某一次事件觸發的時候,上一次事件的處理方法還沒有機會執行,仍然在等待隊列中。這個時候,這個新的定時器事件就被丟棄。需要注意的是,由於JavaScript引擎的這種執行方式,兩次執行定時器事件處理方法的實際時間間隔小於設定的時間間隔。例如,在上一個定時器事件的處理方法觸發之後,等待了50 ms才獲得被執行的機會,而第二個定時器事件的處理方法被觸發後馬上就被執行了。也就是說,這兩者之間的時間間隔實際上只有50 ms。因此,setInterval並不適合實現精確的按固定間隔的調度操作。

總的來說,使用setTimeout和setInterval都不能滿足精確的時間間隔。通過setTimeout設置的JavaScript方法的實際執行間隔不小於設定的時間,而通過setInterval設置的重複執行的JavaScript方法的間隔可能會小於設定的時間。

setTimeout可用於設置在某個時間間隔之後再執行某個JavaScript方法。setTimeout的另外一個作用是可以將某些操作推遲執行,讓出JavaScript引擎來處理其等待隊列中的其他事件,以提高用戶體驗。例如,某個操作需要進行大量計算,平均耗時在3 s左右,當這個操作開始執行之後,就會一直佔用JavaScript引擎,直到執行結束。在這個過程中,其他的JavaScript方法就被放置到JavaScript引擎的等待隊列中。如果用戶在這個過程中單擊了頁面上的某個按鈕,那麼相應的事件處理方法並不能馬上執行,給用戶的感覺就是整個Web應用暫時失去了響應,給用戶帶來不好的用戶體驗。

如果使用setTimeout,就可以把一個需要較長執行時間的任務分成若干個小任務,這些小任務之間用setTimeout串聯起來。在這些小任務的執行間隔中,就可以給其他正在等待的JavaScript方法以執行機會。

這裡以計算100 000以內的質數的個數為例進行介紹。求質數的方法有不少,這裡使用一種簡單的方法。對於每一個正整數,通過判斷其是否能被小於或等於其平方根的整數整除,就可以確定其是否為質數。實現的代碼如下:


function isPrime(n){

if(n==0||n==1){

return false;

}

var bound=Math.floor(Math.sqrt(n));

for(var i=2;i<=bound;i++){

if(n%i==0){

return false;

}

}

return true;

}


如果使用一般的計算方式,那麼只需要通過一個很大的循環對範圍之內的每個整數都進行判斷即可。


function calculateNormal{

var count=0;

for(var i=2;i<=MAX;i++){

if(isPrime(i)){

count++;

}

}

alert(\"計算完成,質數個數為:\"+count);

}


上面代碼的問題在於:在整個計算過程中,JavaScript引擎被全部佔用,整個頁面無法響應用戶的其他請求,頁面會呈現「假死」的狀態。而通過setTimeout可以把計算任務分成若干個小任務來執行,提高頁面的響應能力。實現的代碼如下:


function calculateUsingTimeout{

var jobs=10,numberPerJob=Math.ceil(MAX/jobs);

var count=0;

function calculate(start,end){

for(var i=start;i<=end;i++){

if(isPrime(i)){

count++;

}

}

}

var start,end,timeout,finished=0;

function manage{

if(finished==jobs){

window.clearTimeout(timeout);

alert(\"計算完成,質數個數為:\"+count);

}

else{

start=finished*numberPerJob+1,

end=Math.min((finished+1)*numberPerJob,MAX);

timeout=window.setTimeout(function{

calculate(start,end);

finished++;

manage;

},100);

}

}

manage;

}


通過setTimeout把耗時較長的計算任務分成了10個小任務,每個任務之間的執行間隔是100 ms。在這些小任務的執行間隔中,JavaScript引擎是可以處理其他事件的,這樣就保證了對用戶的響應時間。雖然整個任務總的執行時間變長了,但是帶來了用戶體驗的提升。