512
技術社區[雲棲]
secrets of the javascript Ninja( javascript Timer)(javascript忍者的秘密)
javascript中定時器是如何工作的
計時器(定時器)是一個大家常常不能夠充分理解,並且常常被誤用的JavaScript的特性之一,使用計時器能夠開發非常複雜的動態效果,它具有在一段時間後異步執行一段代碼的能力,由於JavaScript天生是單線程的,但是使用計時器能夠打到異步執行一段代碼的能力。
從根本上理解定時器是如何工作的是非常重要的,由於它是單線程運作,定時器如何運作並不是太明了,下麵我們通過一下三個方法,來看看它到底是如何執行的,這三個方法能夠形成和操作計時器:
var id = setTimeout(fn, delay);該方法初始化一個單獨的計時器,用於在delay指定的時間後調用fn函數,該方法會產生一個唯一的ID,稍後可以使用該ID取消該計時器
var id = setInterval(fn, delay); 該方法和上麵的方法類似,他會在每隔delay指定的時間後都會都會執行fn函數,也會產生一個唯一的ID,用於取消定時器。
clearInterval(id);, clearTimout(id);接受上述兩個函數返回的ID,停止定時器調用上麵兩個函數中的第一個參數。
為了理解定義器內部是如何工作的,有一個重要的概念我們需要理解:定時器延遲時間並不能保證。因為所有的JavaScript在瀏覽器中執行一個單線程的異步事件時,隻有當有一個入口時才能開始執行。
通過下圖,我們能夠更好的理解這個原因:
在這個示例中有很多信息可以挖掘,但是完全理解了之後你將會更清楚地認識到異步的JavaScript是怎麼執行的。這是個一維的圖:豎直方向上的是(掛鍾式)時間,單位為毫秒。藍色的框表示正在執行的JavaScript片段。舉例來說,第一塊JavaScript執行了約18ms,而鼠標點擊則執行了約11ms,以此類推。
由於JavaScript向來都隻能在同一時間執行一塊代碼(這是由它單線程的本質決定的),所以每一個代碼塊都“阻塞”了其他的異步事件。這意味著當異步事件發生時(比如鼠標點擊、timer觸發或者是XMLHttpRequest完成),這些事件將進入到一個隊列中等待執行(隊列的實現方法因瀏覽器而異,我們在此隻討論一個簡化的情況)。
首先,在第一個JavaScript塊中,有兩個timer被初始化了:一個10ms的setTimeout和一個是10ms的setInterval。由於timer(這裏的timer指setTimeout中的 timer,而下文中的interval則指setInvertal中的timer)開始的時間,實際上它在第一個代碼塊結束前就已經觸發了。然而請注意,它並不會馬上執行(事實上由於單線程的存在,它也無法做到馬上執行)。相反的,這個被延期執行的函數進入隊列中,等待在空閑的時候被執行。
在第一個JavaScript塊中,我們看到一個鼠標點擊事件也發生了。而與這個異步事件(我們不知道用戶什麼時候會去執行一個動作,因此將其認為是一個異步動作)相關的JavaScript回調函數也無法立馬執行,正如timer一樣,它也進行到隊列中等待被執行。
當第一個JavaScript塊被執行完之後,瀏覽器問了一個問題:有正在等待被執行的代碼嗎?在這個例子中,鼠標點擊事件和time事件都正在隊列中等待。於是瀏覽器選了一個(鼠
標點擊事件),然後馬上執行它。而timer隻能繼續等下去。
注意當鼠標點擊事件正在執行的時候第一次的interval事件也觸發了,與timer一樣,它的事件也進入隊列等待之後執行。然而,注意,當interval再次觸發的時候(這個時候timer的事件正在執行),這一次它的事件被丟棄了。如果你在一個大的JavaScript代碼塊正在執行的時候把所有的interval回調函數都囤起來的話,其結果就是在JavaScript代碼塊執行完了之後會有一堆的interval事件被執行,而執行過程中不會有間隔。因此,取代的作法是瀏覽器情願先等一等,以確保在一個interval進入隊列的時候隊列中沒有別的interval。
事實上,我們可以在例子中看出:當第三個interval觸發的時候這個interval自身正在執行。這告訴我們一個重要的事實:interval是不管當前在執行些什麼的,在任何情況下它都會進入到隊列中去,即使這樣意味著每次回調之間的時間就不準確了。
最後,當第二個interval回調執行完後,我們可以看到隊列已經被清空,沒有什麼需要JavaScript引擎去執行的了。這表明瀏覽器現在等待一個新的異步事件發生。於是在50ms的
時候我們看到interval又觸發了。這一樣,由於沒有什麼東西擋住了它的執行,它馬上就觸發了。
讓我們來看一個例子,這個例子更好地闡釋了setTimeout和setInveral之間的區別。
setTimeout(function(){ /* 一個很長的代碼塊…… */ setTimeout(arguments.callee, 10); }, 10); setInterval(function(){ /* 一個很長的代碼塊…… */ }, 10);
乍看上去,這兩段代碼在功能上似乎是相同的,可實際上並非如此。setTimeout的代碼在前一次的回調執行完後總是至少會有10ms的延時(有可能會更多,但是絕對不會更少);而setInterval則總是在每10ms的時候嚐試執行一次回調,它不管上一次回調是什麼時候執行的。
我們在此學到了很多,讓我們重述一下:
* JavaScript引擎隻有一個線程,這使得異步事件必需列隊等待執行。
* setTimeout和setInterval在如何執行代碼上有著本質地區別。
* 如果一個timer在將要執行的時候被阻塞,它將會等待下一個時機(比預期的延時長)。
* 如果interval的執行時間較長(比指定的延時長),那麼它們將連續地執行而沒有延時。
什麼地方使用JavaScript定時器
- 大量處理dom時,我們可以采用分步加載的辦法。
我們通過下麵的代碼理解這種用法:
var table = document.getElementsByTagName("tbody"); for ( var i = 0; i < 2000; i++ ) { var tr = document.createElement("tr"); for ( var t = 0; t < 6; t++ ){ var td = document.createElement("td"); td.appendChild( document.createTextNode(" "+t )); tr.appendChild( td ); } table[0].appendChild( tr ); } //如果我們采用分步加載呢; var i = 0, max = 1999; setTimeout(function(){ for ( var step = i + 500; i < step; i++ ) { var tr = document.createElement("tr"); for ( var t = 0; t < 6; t++ ){ var td = document.createElement("td"); td.appendChild( document.createTextNode("" + t)); tr.appendChild( td ); } table[0].appendChild( tr ); } if ( i < max ) setTimeout( arguments.callee, 0 ); }, 0);
- 中央定時器控製
當大量使用定時器時,我們該如何管理這些定時器,這種情況在動畫處理中尤其重要,因為需要同時處理大量的屬性,所以需要一種方案管理這些。
var timers = { timerID: 0, timers: [], start: function(){ if ( this.timerID ) return; (function(){ for ( var i = 0; i < timers.timers.length; i++ ) if ( timers.timers[i]() === false ) { timers.timers.splice(i, 1); i--; } timers.timerID = setTimeout( arguments.callee, 100 ); })(); }, stop: function(){ clearTimeout( this.timerID ); this.timerID = 0; }, add: function(fn){ this.timers.push( fn ); this.start(); } };
通過使用這樣一個定時器,我們可以了解到一下一個好處:
1.在同意時間一個頁麵上隻有一個定時器在執行。
2.你可以根據你的意誌暫停或者重新開始定時器。
3.你可以頻繁的處理刪除的回調函數。
下麵來看看如果使用這個中央定時控製器:
<div mce_>Hello!</div> var d=document,box = d.getElementById("box"), left = 0, top = 20; 使用的時候隻需要: timers.add(function(){ box.style.left = left + "px"; if ( ++left > 50 ) return false; }); timers.add(function(){ box.style.top = top + "px"; top += 2; if ( top > 120 ) return false; }); };
最後更新:2017-04-02 00:06:44