1 先看下兩個例子
1.1. 簡單的settimeout
setTimeout(function () { while (true) { } }, 1000);
setTimeout(function () { alert('end 2'); }, 2000);
setTimeout(function () { alert('end 1'); }, 100);
alert('end');
執行的結果是彈出’end’、’end 1’,然後瀏覽器假死,就是不彈出‘end 2’。也就是說第一個settimeout裏執行的時候是一個死循環,這個直接導致了理論上比它晚一秒執行的第二個settimeout裏的函數被阻塞,這個和我們平時所理解的異步函數多線程互不干擾是不符的。
附計時器使用方法
--初始化一個簡單的js的計時器,一段時間後,才觸發並執行回調函數。 setTimeout 返回一個唯一id,可用這個id來取消這個計時器。
var id = setTimeout(fn,delay);
--類似於setTimeout,不一樣的是,每隔一段時間,會持續調用回調fn,直到被取消
var id = setInterval(fn,delay);
--傳入一個計時器的id,取消計時器。
clearInterval(id);
clearTimeout(id);
1.2. ajax請求回調
接着我們來測試一下通過xmlhttprequest實現ajax異步請求調用,主要代碼如下:
var xmlReq = createXMLHTTP();//創建一個xmlhttprequest對象
function testAsynRequest() {
var url = "/AsyncHandler.ashx?action=ajax";
xmlReq.open("post", url, true);
xmlReq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlReq.onreadystatechange = function () {
if (xmlReq.readyState == 4) {
if (xmlReq.status == 200) {
var jsonData = eval('(' + xmlReq.responseText + ')');
alert(jsonData.message);
}
else if (xmlReq.status == 404) {
alert("Requested URL is not found.");
} else if (xmlReq.status == 403) {
alert("Access denied.");
} else {
alert("status is " + xmlReq.status);
}
}
};
xmlReq.send(null);
}
testAsynRequest();//1秒後調用回調函數
while (true) {
}
在服務端實現簡單的輸出:
private void ProcessAjaxRequest(HttpContext context)
{
string action = context.Request["ajax"];
Thread.Sleep(1000);//等1秒
string jsonObject = "{\"message\":\"" + action + "\"}";
context.Response.Write(jsonObject);
}
理論上,如果ajax異步請求,它的異步回調函數是在單獨一個線程中,那麼回調函數必然不被其他線程”阻撓“而順利執行,也就是1秒後,它回調執行彈出‘ajax’,可是實際情況並非如此,回調函數無法執行,因爲瀏覽器再次因爲死循環假死。
據上面兩個例子,總結如下:
- JavaScript引擎是單線程運行的,瀏覽器無論在什麼時候都只且只有一個線程在運行JavaScript程序。
- JavaScript引擎用單線程運行也是有意義的,單線程不必理會線程同步這些複雜的問題,問題得到簡化。
在瞭解計時器內部運作前,我們必須清楚一點,觸發和執行並不是同一概念,計時器的回調函數一定會在指定delay的時間後被觸發,但並不一定立即執行,可能需要等待。所有JavaScript代碼是在一個線程裏執行的,像鼠標點擊和計時器之類的事件只有在JS單線程空閒時才執行。
我們來看一下圖表,一開始你可能並沒發現什麼或啥都不懂,但請靜下心來,在腦海裏繪製出這個場景
由於JavaScript引擎同一時間只執行一段代碼(這是由JavaScript單線程的性質決定的),所以每個JS代碼塊阻塞了其它異步事件的進行。這意味着當一個異步事件(像鼠標點擊、計時器、Ajax)發生時,這些事件的回調函數將排在隊列後面等待執行(如何排隊完全取決於各瀏覽器,而我們可以忽視它們內部差異,作一個簡化處理)。
我們首先從第一個JS代碼塊開始,有兩個計時器被初始化:一個10ms的setTimeout和一個10ms的setInterval.觀察計時器初始化位置,(計時器初始化完畢後就會開始計時),發現setTimeout計時器的回調實際上會在第一個代碼塊執行完畢前被觸發。但是這裏注意的是,它不會立即執行(單線程不能這樣做)。實際上,觸發的回調將被排成一個隊列,等待下一個可執行時間。
此外,在第一個JS代碼塊,我們發現一個鼠標點擊事件被觸發。這個鼠標點擊JS回調被綁定在異步隊列上(我們從來不知道用戶什麼時候執行這個操作,所以它被認爲是異步的)且不能馬上執行。像初始化的計時器一樣,排隊等待執行。
執行完初始化JS代碼塊後,瀏覽器就有個疑問:誰在等待執行?此時,鼠標點擊回調和setTimeout計時器的回調都在等待。瀏覽器將選一個(鼠標點擊事件)並立馬執行。而計時器的回調將等待下一合適時機執行。
注意,鼠標點擊事件執行過程中,interval的回調第一次被觸發,與setTimeout的回調一樣,排隊等待執行。隨着時間推移,等到setTimeout計時器的回調執行時候,setInterval的回調再次被觸發,這次被觸發的回調將被拋棄。如果一大段代碼塊正在執行,所有的setInterval的回調都將要排隊,一旦大段代碼塊執行完畢,這些一連串的setInterval的回調相互間將被無延遲地執行。實際上,瀏覽器處理setInterval被觸發的回調排隊等待執行時,除非隊列中setInterval回調爲空,才允許新的setInterval的回調加入。
我們發現,setInterval的第一個被觸發的回調執行時,setInterval的回調又被觸發且排到隊列。這向我們傳達一個重要的消息:setInterval不關心目前JS正在執行的內容,setInterval的被觸發的回調都將會無差別地排隊。
最後,當setInterval的回調執行兩次後,我們發現沒有javascript引擎要執行東西。這意味着瀏覽器將等待着一個新的異步事件發生。我們知道,在50ms時候,setInterval的回調再次被觸發,但這次並沒有東西阻塞,所以回調就立馬執行了。
在瀏覽器中,JavaScript引擎是基於事件驅動的,這裏的事件可看作是瀏覽器派給它的各種任務,這些任務可能源自當前執行的代碼塊,如調用setTimeout(),也可能來自瀏覽器內核,如onload()、onclick()、onmouseover()、setTimeOut()、setInterval()、Ajax等。如果從代碼的角度來看,所謂的任務實體就是各種回調函數,由於“單線程”的原因,這些任務會進行排隊,一個接着一個等待着被引擎處理。(這段說法來源於http://www.benben.cc/blog/?p=327)
3. JavaScript引擎線程和其它偵聽線程
瀏覽器內核實現允許多個線程異步執行,這些線程在內核制控下相互配合以保持同步。假如某一瀏覽器內核的實現至少有三個常駐線程: JavaScript引擎線程,事件觸發線程,Http請求線程,下面通過一個圖來闡明單線程的JavaScript引擎與另外那些線程是怎樣互動通信的。雖然每個瀏覽器內核實現細節不同,但這其中的調用原理都是大同小異。
線程間通信:JavaScript引擎執行當前的代碼塊,其它諸如setTimeout給JS引擎添加一個任務,也可來自瀏覽器內核的其它線程,如界面元素鼠標點擊事件,定時觸發器時間到達通知,異步請求狀態變更通知等.從代碼角度看來任務實體就是各種回調函數,JavaScript引擎一直等待着任務隊列中任務的到來.由於單線程關係,這些任務得進行排隊,一個接着一個被引擎處理.
GUI渲染也是在引擎線程中執行的,腳本中執行對界面進行更新操作,如添加結點,刪除結點或改變結點的外觀等更新並不會立即體現出來,這些操作將保存在一個隊列中,待JavaScript引擎空閒時纔有機會渲染出來。來看例子(這塊內容還有待驗證,個人覺得當Dom渲染時,纔可阻止渲染)
<div id="test">test</div>
<script type="text/javascript" language="javascript">
var i=0;
while(1) {
document.getElementById("test").innerHTML+=i++ + "<br />";
}
</script>
這段代碼的本意是從0開始順序顯示數字,它們將一個接一個出現,現在我們來仔細研究一下代碼,while(1)創建了一個無休止的循環,但是對於單線程的JavaScript引擎而言,在實際情況中就會造成瀏覽器暫停響應並處於假死狀態。
alert()會停止JS引擎的執行,直到按確認鍵,在JS調試的時候,查看當前實時頁面的內容。
4. setTimeout和 setInterval
回到文章開頭,我們來看下setTimeout和setsetInterval的區別。
setTimeout(function(){
/* Some long block of code ... */
setTimout(arguments.callee,10);
},10);
setInterval(function(){
/* Some long block of code ... */
},10);
這兩個程序段第一眼看上去是一樣的,但並不是這樣。setTimeout代碼至少每隔10ms以上才執行一次;然而setInterval固定每隔10ms將嘗試執行,不管它的回調函數的執行狀態。
我們來總結下:
2. setTimeout和setInterval在異步執行時,有着根本性不同。
3. 如果一個計時器被阻塞執行,它將會延遲,直到下一個可執行點(這可能比期望的時間更長)
4. setInterval的回調可能被不停的執行,中間沒間隔(如果回調執行的時間超過預定等待的值)
《JavaScript高級程序設計》中,針對setInterval說法如下:
當使用setInterval()時,僅當沒有該定時器的任何其他代碼實例時,纔將定時器代碼添加到隊列中。還要注意兩問題:
① 某些間隔會被跳過(拋棄);
② 多個定時器的代碼執行之間的間隔可能會比預期小。此時可採取 setTimeout和setsetInterval的區別 的例子方法。
很多同學朋友搞不清楚,既然說JavaScript是單線程運行的,那麼XMLHttpRequest在連接後是否真的異步?其實請求確實是異步的,不過這請求是由瀏覽器新開一個線程請求(參見上圖),當請求的狀態變更時,如果先前已設置回調,這異步線程就產生狀態變更事件放到JavaScript引擎的處理隊列中等待處理,當任務被處理時,JavaScript引擎始終是單線程運行回調函數,具體點即還是單線程運行onreadystatechange所設置的函數。
Tip:理解JavaScript引擎運作非常重要,特別是在大量異步事件(連續)發生時,可以提升程序代碼的效率。
原外文:http://ejohn.org/blog/how-javascript-timers-work/
翻譯參考:http://www.phpweblog.net/rainman/archive/2009/01/05/6267.html
部分示例:http://www.cnblogs.com/jeffwongishandsome/archive/2011/06/13/2080145.html
其它參考:http://www.laruence.com/2009/09/23/1089.html
http://www.benben.cc/blog/?p=327