咱們常常說JS是單線程的,好比node.js研討會上你們都說JS的特點之一是單線程的,這樣使JS更簡單明瞭,但是你們真的理解所謂JS的單線程機制嗎?單線程時,基於事件的異步機制又該當如何,這些知識在《JavaScript權威指南》並無介紹,我也一直困惑了,直到看到一篇外文,纔有了些眉目,這裏與你們分享下。後來發現《JavaScript高級程序設計》高級定時器和循環定時器介紹過,不過以爲沒我翻譯這篇原文介紹得更透徹,以爲我寫的很差的,能夠查看原外文。javascript
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裏的函數被阻塞,這個和咱們平時所理解的異步函數多線程互不干擾是不符的。php
附計時器使用方法html
// 初始化一個簡單的js的計時器,一段時間後,才觸發並執行回調函數。 setTimeout 返回一個惟一id,可用這個id來取消這個計時器。 var id = setTimeout(fn,delay); // 相似於setTimeout,不同的是,每隔一段時間,會持續調用回調fn,直到被取消 var id = setInterval(fn,delay); // 傳入一個計時器的id,取消計時器。 clearInterval(id); clearTimeout(id);
接着咱們來測試一下經過xmlhttprequest實現ajax異步請求調用,主要代碼以下:java
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) { }
在服務端實現簡單的輸出:node
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’,但是實際狀況並不是如此,回調函數沒法執行,由於瀏覽器再次由於死循環假死。web
據上面兩個例子,總結以下:ajax
可JS內部究竟如何實現,咱們在接下來探討。json
在瞭解計時器內部運做前,咱們必須清楚一點,觸發和執行並非同一律念,計時器的回調函數必定會在指定delay的時間後被觸發,但並不必定當即執行,可能須要等待。全部JavaScript代碼是在一個線程裏執行的,像鼠標點擊和計時器之類的事件只有在JS單線程空閒時才執行。瀏覽器
咱們來看一下圖表,一開始你可能並沒發現什麼或啥都不懂,但請靜下心來,在腦海裏繪製出這個場景多線程
這個圖表中有許多數據信息等着咱們去理解,當你徹底理解了這個圖,你會對js的異步運行機制(即JavaScript引擎如何實現異步事件)有很好的瞭解。這個圖是一維的,垂直線上是以毫秒計位,藍色塊表明被劃分的不一樣的js區域執行代碼。例如,第一個JS區塊執行了18毫秒,鼠標點擊事件被阻塞了將近11毫秒,等等。
因爲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回調函數之間的時間間隔會被犧牲掉(縮減)。
最後,當setInterval的回調執行兩次後,咱們發現沒有javascript引擎要執行東西。這意味着瀏覽器將等待着一個新的異步事件發生。咱們知道,在50ms時候,setInterval的回調再次被觸發,但此次並無東西阻塞,因此回調就立馬執行了。
在瀏覽器中,JavaScript引擎是基於事件驅動的,這裏的事件可看做是瀏覽器派給它的各類任務,這些任務可能源自當前執行的代碼塊,如調用setTimeout(),也可能來自瀏覽器內核,如onload()、onclick()、onmouseover()、setTimeOut()、setInterval()、Ajax等。若是從代碼的角度來看,所謂的任務實體就是各類回調函數,因爲「單線程」的緣由,這些任務會進行排隊,一個接着一個等待着被引擎處理。(這段說法來源於PaulGuo)
注意:
一秒一次的setInterver()
執行時插入一個耗時10s的setTimeout,能夠看到setInterver是繼續執行的,而不是一下蹦出10個setInterver()的回調。
瀏覽器是多線程的,它們在內核制控下相互配合以保持同步。一個瀏覽器至少實現三個常駐線程:JavaScript引擎線程,GUI渲染線程,瀏覽器事件觸發線程(UI線程)。
javascript要等主線程空了纔會去查看子線程有沒有回調內容。異步的任務執行的順序是不固定的,主要看返回的速度。
瀏覽器內核實現容許多個線程異步執行,這些線程在內核制控下相互配合以保持同步。假如某一瀏覽器內核的實現至少有三個常駐線程: javascript引擎線程,界面渲染線程,瀏覽器事件觸發線程,除些之外,也有一些執行完就終止的線程,如Http請求線程,這些異步線程都會產生不一樣的異步事件,下面經過一個圖來闡明單線程的JavaScript引擎與另外那些線程是怎樣互動通訊的.雖然每一個瀏覽器內核實現細節不一樣,但這其中的調用原理都是大同小異.
上圖中,定時器和事件都按時觸發了,這代表JavaScript引擎的線程和計時器觸發線程、事件觸發線程是三個單獨的線程,即便JavaScript引擎的線程被阻塞,其它兩個觸發線程都在運行。
線程間通訊:JavaScript引擎執行當前的代碼塊,其它諸如setTimeout給JS引擎添加一個任務,也可來自瀏覽器內核的其它線程,如界面元素鼠標點擊事件,定時觸發器時間到達通知,異步請求狀態變動通知等.從代碼角度看來任務實體就是各類回調函數,JavaScript引擎一直等待着任務隊列中任務的到來.因爲單線程關係,這些任務得進行排隊,一個接着一個被引擎處理
GUI渲染也是在引擎線程中執行的(準確描述見下),腳本中執行對界面進行更新操做,如添加結點,刪除結點或改變結點的外觀等更新並不會當即體現出來,這些操做將保存在一個隊列中,待JavaScript引擎空閒時纔有機會渲染出來。來看例子(這塊內容還有待驗證,我的以爲當Dom渲染時,纔可阻止渲染)
渲染線程與JavaScript引擎線程是互斥的,這容易理解,由於JavaScript腳本是可操縱DOM元素,在修改這些元素屬性同時渲染界面,那麼渲染線程先後得到的元素數據就可能不一致了.見深刻理解JavaScript定時機制
<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調試的時候,查看當前實時頁面的內容。
回到文章開頭,咱們來看下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將嘗試執行,無論它的回調函數的執行狀態。
咱們來總結下:
《JavaScript高級程序設計》中,針對setInterval說法以下:
當使用setInterval()時,僅當沒有該定時器的任何其餘代碼實例時,纔將定時器代碼添加到隊列中。還要注意兩問題:
不少同窗朋友搞不清楚,既然說JavaScript是單線程運行的,那麼XMLHttpRequest在鏈接後是否真的異步?其實請求確實是異步的,不過這請求是由瀏覽器新開一個線程請求(參見上圖),當請求的狀態變動時,若是先前已設置回調,這異步線程就產生狀態變動事件放到JavaScript引擎的處理隊列中等待處理,當任務被處理時,JavaScript引擎始終是單線程運行回調函數,具體點即仍是單線程運行onreadystatechange所設置的函數。
Tip:理解JavaScript引擎運做很是重要,特別是在大量異步事件(連續)發生時,能夠提高程序代碼的效率。
網上的帖子大多深淺不一,甚至有些先後矛盾,在下的文章都是學習過程當中的總結,若是發現錯誤,歡迎留言指出~
參考:
深刻理解JavaScript定時機制
原外文
翻譯參考
部分示例
其它參考1, 參考2