我們常說JS是單線程的,比如Node.js研討會上大家都說JS的特色之一是單線程的,這樣使JS更簡單明了,可是大家真的理解所謂JS的單線程機制嗎?單線程時,基於事件的非同步機制又該如何,這些知識在《JavaScript權威指南》並沒有介紹,我也一直困惑了,直到看到一篇外文,才有了些眉目,這裡與大家分享下。翻譯的過程中,發現已有人翻譯了這篇文章,於是乎,在某些語句上,借鑒了下。文章網址:連結。後來發現《JavaScript高級程式設計》高級定時器和循環定時器介紹過,不過覺得沒我翻譯這篇原文介紹得更透徹,覺得我寫的不好的,可以查看原外文
1 先看下兩個例子
1.1. 簡單的settimeout
setTimeout(function () { while (true) { } }, 1000); setTimeout(function () { alert('end 2'); }, 2000); setTimeout(function () { alert('end 1'); }, 100); alert('end');
執行的結果是彈出式'end'、'endend 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引擎用单线程运行也是有意义的,单线程不必理会线程同步这些复杂的问题,问题得到简化。
2. JavaScript引擎
可JS內部究竟如何實現,我們在接下來探討。
在了解計時器內部運作前,我們必須清楚一點,觸發和執行並不是同一概念,計時器的回調函數一定會在指定delay的時間後被觸發,但並不一定立即執行,可能需要等待。所有JavaScript程式碼是在一個執行緒裡執行的,像是滑鼠點擊和計時器之類的事件只有在JS單執行緒空閒時才執行。
JS 的執行緒、事件循環、任務佇列簡介
JS 是單執行緒的,但卻能執行非同步任務,這主要是因為JS 中存在事件循環(Event Loop)和任務佇列(Task Queue)。
事件循環:JS 會建立一個類似 while (true) 的循環,每執行一次循環體的過程稱之為 Tick。每次 Tick 的過程就是查看是否有待處理事件,如果有則取出相關事件及回呼函數放入執行棧中由主執行緒執行。待處理的事件會儲存在一個任務佇列中,也就是每次 Tick 都會查看任務佇列中是否有需要執行的任務。
任務佇列:非同步操作會將相關回呼加入到任務佇列中。而不同的非同步操作加入到任務佇列的時機也不同,如onclick, setTimeout, ajax 處理的方式都不同,這些非同步操作是由瀏覽器核心的webcore 來執行的,webcore 包含上圖中的3種webAPI,分別是DOM Binding、network、timer模組。
onclick 由瀏覽器核心的 DOM Binding 模組來處理,當事件觸發的時候,回呼函數會立即加入到任務佇列中。
setTimeout 會由瀏覽器核心的 timer 模組來進行延時處理,當時間到達的時候,才會將回呼函數加入到任務佇列中。
ajax 則會由瀏覽器核心的 network 模組來處理,在網路要求完成返回之後,才將回調加入到任務佇列中。
主线程:JS 只有一个线程,称之为主线程。而事件循环是主线程中执行栈里的代码执行完毕之后,才开始执行的。所以,主线程中要执行的代码时间过长,会阻塞事件循环的执行,也就会阻塞异步操作的执行。只有当主线程中执行栈为空的时候(即同步代码执行完后),才会进行事件循环来观察要执行的事件回调,当事件循环检测到任务队列中有事件就取出相关回调放入执行栈中由主线程执行。
Update:
《你不知道的 JavaScript》一书中,重新讲解了 ES6 新增的任务队列,和上面的任务队列略有不同,上面的任务队列书中称为事件队列。
上面提到的任务(事件)队列是在事件循环中的,事件循环每一次 tick 便执行上面所述的任务(事件)队列中的一个任务。而任务(事件)队列是只能往尾部添加任务。
而 ES6 中新增的任务队列是在事件循环之上的,事件循环每次 tick 后会查看 ES6 的任务队列中是否有任务要执行,也就是 ES6 的任务队列比事件循环中的任务(事件)队列优先级更高。
如 Promise 就使用了 ES6 的任务队列特性。
3. JavaScript引擎线程和其它侦听线程
在浏览器中,JavaScript引擎是基于事件驱动的,这里的事件可看作是浏览器派给它的各种任务,这些任务可能源自当前执行的代码块,如调用setTimeout(),也可能来自浏览器内核,如onload()、onclick()、onmouseover()、setTimeOut()、setInterval()、Ajax等。如果从代码的角度来看,所谓的任务实体就是各种回调函数,由于“单线程”的原因,这些任务会进行排队,一个接着一个等待着被引擎处理。
上图中,定时器和事件都按时触发了,这表明JavaScript引擎的线程和计时器触发线程、事件触发线程是三个单独的线程,即使JavaScript引擎的线程被阻塞,其它两个触发线程都在运行。
浏览器内核实现允许多个线程异步执行,这些线程在内核制控下相互配合以保持同步。假如某一浏览器内核的实现至少有三个常驻线程: JavaScript引擎线程,事件触发线程,Http请求线程,下面通过一个图来阐明单线程的JavaScript引擎与另外那些线程是怎样互动通信的。虽然每个浏览器内核实现细节不同,但这其中的调用原理都是大同小异。
线程间通信:JavaScript引擎执行当前的代码块,其它诸如setTimeout给JS引擎添加一个任务,也可来自浏览器内核的其它线程,如界面元素鼠标点击事件,定时触发器时间到达通知,异步请求状态变更通知等.从代码角度看来任务实体就是各种回调函数,JavaScript引擎一直等待着任务队列中任务的到来.由于单线程关系,这些任务得进行排队,一个接着一个被引擎处理.
GUI渲染也是在引擎线程中执行的,脚本中执行对界面进行更新操作,如添加结点,删除结点或改变结点的外观等更新并不会立即体现出来,这些操作将保存在一个队列中,待JavaScript引擎空闲时才有机会渲染出来。来看例子(这块内容还有待验证,个人觉得当Dom渲染时,才可阻止渲染)
<p id="test">test</p> <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将尝试执行,不管它的回调函数的执行状态。
我们来总结下:
l JavaScript引擎只有一个线程,强制异步事件排队等待执行。 l setTimeout和setInterval在异步执行时,有着根本性不同。 l 如果一个计时器被阻塞执行,它将会延迟,直到下一个可执行点(这可能比期望的时间更长) l setInterval的回调可能被不停的执行,中间没间隔(如果回调执行的时间超过预定等待的值)
《JavaScript高级程序设计》中,针对setInterval说法如下:
当使用setInterval()时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。还要注意两问题:
① 某些间隔会被跳过(抛弃); ② 多个定时器的代码执行之间的间隔可能会比预期小。此时可采取 setTimeout和setsetInterval的区别 的例子方法。
5. Ajax异步
很多同學朋友搞不清楚,既然說JavaScript是單執行緒運行的,那麼XMLHttpRequest在連線後是否真的非同步?其實請求確實是異步的,不過這請求是由瀏覽器新開一個線程請求(參見上圖),當請求的狀態變更時,如果先前已設定回調,這非同步線程就產生狀態變更事件放到JavaScript引擎的處理佇列中等待處理,當任務被處理時,JavaScript引擎總是單執行緒執行回呼函數,具體點即還是單執行緒執行onreadystatechange所設定的函數。
Tip:理解JavaScript引擎運作非常重要,特別是在大量非同步事件(連續)發生時,可以提升程式碼的效率。
以上是深入理解js異步原理問題的詳細內容。更多資訊請關注PHP中文網其他相關文章!