摘要:由于引擎同一時間只執(zhí)行一段代碼這是由單線程的性質(zhì)決定的,所以每個代碼塊阻塞了其它異步事件的進行。這意味著瀏覽器將等待著一個新的異步事件發(fā)生。異步的任務執(zhí)行的順序是不固定的,主要看返回的速度。
我們經(jīng)常說JS是單線程的,比如node.js研討會上大家都說JS的特色之一是單線程的,這樣使JS更簡單明了,可是大家真的理解所謂JS的單線程機制嗎?單線程時,基于事件的異步機制又該當如何,這些知識在《JavaScript權(quán)威指南》并沒有介紹,我也一直困惑了,直到看到一篇外文,才有了些眉目,這里與大家分享下。后來發(fā)現(xiàn)《JavaScript高級程序設(shè)計》高級定時器和循環(huán)定時器介紹過,不過覺得沒我翻譯這篇原文介紹得更透徹,覺得我寫的不好的,可以查看原外文。
1 先看下兩個例子 1.1. 簡單的settimeoutsetTimeout(function () { while (true) { } }, 1000); setTimeout(function () { alert("end 2"); }, 2000); setTimeout(function () { alert("end 1"); }, 100); alert("end");
執(zhí)行的結(jié)果是彈出’end’、’end 1’,然后瀏覽器假死,就是不彈出‘end 2’。也就是說第一個settimeout里執(zhí)行的時候是一個死循環(huán),這個直接導致了理論上比它晚一秒執(zhí)行的第二個settimeout里的函數(shù)被阻塞,這個和我們平時所理解的異步函數(shù)多線程互不干擾是不符的。
附計時器使用方法
// 初始化一個簡單的js的計時器,一段時間后,才觸發(fā)并執(zhí)行回調(diào)函數(shù)。 setTimeout 返回一個唯一id,可用這個id來取消這個計時器。 var id = setTimeout(fn,delay); // 類似于setTimeout,不一樣的是,每隔一段時間,會持續(xù)調(diào)用回調(diào)fn,直到被取消 var id = setInterval(fn,delay); // 傳入一個計時器的id,取消計時器。 clearInterval(id); clearTimeout(id);1.2 ajax請求回調(diào)
接著我們來測試一下通過xmlhttprequest實現(xiàn)ajax異步請求調(diào)用,主要代碼如下:
var xmlReq = createXMLHTTP();//創(chuàng)建一個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秒后調(diào)用回調(diào)函數(shù) while (true) { }
在服務端實現(xiàn)簡單的輸出:
private void ProcessAjaxRequest(HttpContext context) { string action = context.Request["ajax"]; Thread.Sleep(1000);//等1秒 string jsonObject = "{"message":"" + action + ""}"; context.Response.Write(jsonObject); }
理論上,如果ajax異步請求,它的異步回調(diào)函數(shù)是在多帶帶一個線程中,那么回調(diào)函數(shù)必然不被其他線程”阻撓“而順利執(zhí)行,也就是1秒后,它回調(diào)執(zhí)行彈出‘a(chǎn)jax’,可是實際情況并非如此,回調(diào)函數(shù)無法執(zhí)行,因為瀏覽器再次因為死循環(huán)假死。
據(jù)上面兩個例子,總結(jié)如下:
JavaScript引擎是單線程運行的,瀏覽器無論在什么時候都只且只有一個線程在運行JavaScript程序.
JavaScript引擎用單線程運行也是有意義的,單線程不必理會線程同步這些復雜的問題,問題得到簡化。
2. JavaScript引擎可JS內(nèi)部究竟如何實現(xiàn),我們在接下來探討。
在了解計時器內(nèi)部運作前,我們必須清楚一點,觸發(fā)和執(zhí)行并不是同一概念,計時器的回調(diào)函數(shù)一定會在指定delay的時間后被觸發(fā),但并不一定立即執(zhí)行,可能需要等待。所有JavaScript代碼是在一個線程里執(zhí)行的,像鼠標點擊和計時器之類的事件只有在JS單線程空閑時才執(zhí)行。
我們來看一下圖表,一開始你可能并沒發(fā)現(xiàn)什么或啥都不懂,但請靜下心來,在腦海里繪制出這個場景
這個圖表中有許多數(shù)據(jù)信息等著我們?nèi)ダ斫猓斈阃耆斫饬诉@個圖,你會對js的異步運行機制(即JavaScript引擎如何實現(xiàn)異步事件)有很好的了解。這個圖是一維的,垂直線上是以毫秒計位,藍色塊代表被劃分的不同的js區(qū)域執(zhí)行代碼。例如,第一個JS區(qū)塊執(zhí)行了18毫秒,鼠標點擊事件被阻塞了將近11毫秒,等等。
由于JavaScript引擎同一時間只執(zhí)行一段代碼(這是由JavaScript單線程的性質(zhì)決定的),所以每個JS代碼塊阻塞了其它異步事件的進行。這意味著當一個異步事件(像鼠標點擊、計時器、Ajax)發(fā)生時,這些事件的回調(diào)函數(shù)將排在隊列后面等待執(zhí)行(如何排隊完全取決于各瀏覽器,而我們可以忽視它們內(nèi)部差異,作一個簡化處理)。
我們首先從第一個JS代碼塊開始,有兩個計時器被初始化:一個10ms的setTimeout和一個10ms的setInterval觀察計時器初始化位置,(計時器初始化完畢后就會開始計時),發(fā)現(xiàn)setTimeout計時器的回調(diào)實際上會在第一個代碼塊執(zhí)行完畢前被觸發(fā)。但是這里注意的是,它不會立即執(zhí)行(單線程不能這樣做)。實際上,觸發(fā)的回調(diào)將被排成一個隊列,等待下一個可執(zhí)行時間。
此外,在第一個JS代碼塊,我們發(fā)現(xiàn)一個鼠標點擊事件被觸發(fā)。這個鼠標點擊JS回調(diào)被綁定在異步隊列上(我們從來不知道用戶什么時候執(zhí)行這個操作,所以它被認為是異步的)且不能馬上執(zhí)行。像初始化的計時器一樣,排隊等待執(zhí)行。
執(zhí)行完初始化JS代碼塊后,瀏覽器就有個疑問:誰在等待執(zhí)行?此時,鼠標點擊回調(diào)和setTimeout計時器的回調(diào)都在等待。瀏覽器將選一個(實際上選擇了“鼠標點擊事件的處理函數(shù)”,因為由圖可知它是先進隊的)并立馬執(zhí)行。而計時器的回調(diào)將等待下一合適時機執(zhí)行。
注意,鼠標點擊事件執(zhí)行過程中,interval的回調(diào)第一次被觸發(fā),與setTimeout的回調(diào)一樣,排隊等待執(zhí)行。隨著時間推移,等到setTimeout計時器的回調(diào)執(zhí)行時候,setInterval的回調(diào)再次被觸發(fā),這次被觸發(fā)的回調(diào)將被拋棄(因為已經(jīng)有個同樣的回調(diào)在排隊了)。如果一大段代碼塊正在執(zhí)行,所有的setInterval的回調(diào)都將要排隊,一旦大段代碼塊執(zhí)行完畢,這些一連串的setInterval的回調(diào)相互間將被無延遲地執(zhí)行。實際上,瀏覽器處理setInterval被觸發(fā)的回調(diào)排隊等待執(zhí)行時,除非隊列中setInterval回調(diào)為空,才允許新的setInterval的回調(diào)加入。
我們發(fā)現(xiàn),setInterval的第一個被觸發(fā)的回調(diào)執(zhí)行時,setInterval的回調(diào)又被觸發(fā)且排到隊列。這向我們傳達一個重要的消息:setInterval不關(guān)心目前JS正在執(zhí)行的內(nèi)容,setInterval的被觸發(fā)的回調(diào)都將會無差別地排隊,這意味著兩次setInterval回調(diào)函數(shù)之間的時間間隔會被犧牲掉(縮減)。
最后,當setInterval的回調(diào)執(zhí)行兩次后,我們發(fā)現(xiàn)沒有javascript引擎要執(zhí)行東西。這意味著瀏覽器將等待著一個新的異步事件發(fā)生。我們知道,在50ms時候,setInterval的回調(diào)再次被觸發(fā),但這次并沒有東西阻塞,所以回調(diào)就立馬執(zhí)行了。
在瀏覽器中,JavaScript引擎是基于事件驅(qū)動的,這里的事件可看作是瀏覽器派給它的各種任務,這些任務可能源自當前執(zhí)行的代碼塊,如調(diào)用setTimeout(),也可能來自瀏覽器內(nèi)核,如onload()、onclick()、onmouseover()、setTimeOut()、setInterval()、Ajax等。如果從代碼的角度來看,所謂的任務實體就是各種回調(diào)函數(shù),由于“單線程”的原因,這些任務會進行排隊,一個接著一個等待著被引擎處理。(這段說法來源于PaulGuo)
注意:
一秒一次的setInterver()執(zhí)行時插入一個耗時10s的setTimeout,可以看到setInterver是繼續(xù)執(zhí)行的,而不是一下蹦出10個setInterver()的回調(diào)。
3. 瀏覽器的三個常駐線程瀏覽器是多線程的,它們在內(nèi)核制控下相互配合以保持同步。一個瀏覽器至少實現(xiàn)三個常駐線程:JavaScript引擎線程,GUI渲染線程,瀏覽器事件觸發(fā)線程(UI線程)。
javascript引擎是基于事件驅(qū)動單線程執(zhí)行的。JS引擎一直等待著event loop中任務的到來,然后加以處理(只有當前函數(shù)執(zhí)行棧執(zhí)行完畢,才會去任務隊列中取任務執(zhí)行)。瀏覽器無論什么時候都只有一個JS線程在運行JS程序。
UI渲染線程負責渲染瀏覽器界面,當界面需要重繪(Repaint)或由于某種操作引發(fā)回流(reflow)時,該線程就會執(zhí)行。但是 GUI渲染線程與JS引擎是互斥的,當JS引擎執(zhí)行時GUI線程會被掛起,JS對頁面的操作即GUI的更新也會被保存在一個隊列中,等到JS引擎空閑時才有機會被執(zhí)行。這就是JS阻塞頁面加載。
事件觸發(fā)線程,當一個事件被觸發(fā)時該線程會把事件添加到任務隊列的隊尾,等待JS引擎的處理。這些事件可以來自JavaScript引擎當前執(zhí)行的代碼塊調(diào)用setTimeout/ajax添加一個任務,也可以來自瀏覽器其他線程如鼠標點擊添加的任務。但由于JS的單線程關(guān)系,所有這些事件都得排隊等待JS引擎處理。
javascript要等主線程空了才會去查看子線程有沒有回調(diào)內(nèi)容。異步的任務執(zhí)行的順序是不固定的,主要看返回的速度。
4. JavaScript引擎線程和其它偵聽線程瀏覽器內(nèi)核實現(xiàn)允許多個線程異步執(zhí)行,這些線程在內(nèi)核制控下相互配合以保持同步。假如某一瀏覽器內(nèi)核的實現(xiàn)至少有三個常駐線程: javascript引擎線程,界面渲染線程,瀏覽器事件觸發(fā)線程,除些以外,也有一些執(zhí)行完就終止的線程,如Http請求線程,這些異步線程都會產(chǎn)生不同的異步事件,下面通過一個圖來闡明單線程的JavaScript引擎與另外那些線程是怎樣互動通信的.雖然每個瀏覽器內(nèi)核實現(xiàn)細節(jié)不同,但這其中的調(diào)用原理都是大同小異.
上圖中,定時器和事件都按時觸發(fā)了,這表明JavaScript引擎的線程和計時器觸發(fā)線程、事件觸發(fā)線程是三個多帶帶的線程,即使JavaScript引擎的線程被阻塞,其它兩個觸發(fā)線程都在運行。
線程間通信:JavaScript引擎執(zhí)行當前的代碼塊,其它諸如setTimeout給JS引擎添加一個任務,也可來自瀏覽器內(nèi)核的其它線程,如界面元素鼠標點擊事件,定時觸發(fā)器時間到達通知,異步請求狀態(tài)變更通知等.從代碼角度看來任務實體就是各種回調(diào)函數(shù),JavaScript引擎一直等待著任務隊列中任務的到來.由于單線程關(guān)系,這些任務得進行排隊,一個接著一個被引擎處理
GUI渲染也是在引擎線程中執(zhí)行的(準確描述見下),腳本中執(zhí)行對界面進行更新操作,如添加結(jié)點,刪除結(jié)點或改變結(jié)點的外觀等更新并不會立即體現(xiàn)出來,這些操作將保存在一個隊列中,待JavaScript引擎空閑時才有機會渲染出來。來看例子(這塊內(nèi)容還有待驗證,個人覺得當Dom渲染時,才可阻止渲染)
渲染線程與JavaScript引擎線程是互斥的,這容易理解,因為JavaScript腳本是可操縱DOM元素,在修改這些元素屬性同時渲染界面,那么渲染線程前后獲得的元素數(shù)據(jù)就可能不一致了.見深入理解JavaScript定時機制
test
這段代碼的本意是從0開始順序顯示數(shù)字,它們將一個接一個出現(xiàn),現(xiàn)在我們來仔細研究一下代碼,while(1)創(chuàng)建了一個無休止的循環(huán),但是對于單線程的JavaScript引擎而言,在實際情況中就會造成瀏覽器暫停響應并處于假死狀態(tài)。
alert()會停止JS引擎的執(zhí)行,直到按確認鍵,在JS調(diào)試的時候,查看當前實時頁面的內(nèi)容。
5. setTimeout和 setInterval回到文章開頭,我們來看下setTimeout和setsetInterval的區(qū)別。
setTimeout(function(){ /* Some long block of code ... */ setTimout(arguments.callee,10); },10); setInterval(function(){ /* Some long block of code ... */ },10);
這兩個程序段第一眼看上去是一樣的,但并不是這樣。setTimeout代碼至少每隔10ms以上才執(zhí)行一次;然而setInterval固定每隔10ms將嘗試執(zhí)行,不管它的回調(diào)函數(shù)的執(zhí)行狀態(tài)。
我們來總結(jié)下:
JavaScript引擎只有一個線程,強制異步事件排隊等待執(zhí)行。
setTimeout和setInterval在異步執(zhí)行時,有著根本性不同。
如果一個計時器被阻塞執(zhí)行,它將會延遲,直到下一個可執(zhí)行點(這可能比期望的時間更長)
setInterval的回調(diào)可能被不停的執(zhí)行,中間沒間隔(如果回調(diào)執(zhí)行的時間超過預定等待的值)
《JavaScript高級程序設(shè)計》中,針對setInterval說法如下:
當使用setInterval()時,僅當沒有該定時器的任何其他代碼實例時,才將定時器代碼添加到隊列中。還要注意兩問題:
某些間隔會被跳過(拋棄);
多個定時器的代碼執(zhí)行之間的間隔可能會比預期小。此時可采取 setTimeout和setsetInterval的區(qū)別 的例子方法。
5. Ajax異步很多同學朋友搞不清楚,既然說JavaScript是單線程運行的,那么XMLHttpRequest在連接后是否真的異步?其實請求確實是異步的,不過這請求是由瀏覽器新開一個線程請求(參見上圖),當請求的狀態(tài)變更時,如果先前已設(shè)置回調(diào),這異步線程就產(chǎn)生狀態(tài)變更事件放到JavaScript引擎的處理隊列中等待處理,當任務被處理時,JavaScript引擎始終是單線程運行回調(diào)函數(shù),具體點即還是單線程運行onreadystatechange所設(shè)置的函數(shù)。
Tip:理解JavaScript引擎運作非常重要,特別是在大量異步事件(連續(xù))發(fā)生時,可以提升程序代碼的效率。
網(wǎng)上的帖子大多深淺不一,甚至有些前后矛盾,在下的文章都是學習過程中的總結(jié),如果發(fā)現(xiàn)錯誤,歡迎留言指出~
參考:
深入理解JavaScript定時機制
原外文
翻譯參考
部分示例
其它參考1,參考2
PS:歡迎大家關(guān)注我的公眾號【前端下午茶】,一起加油吧~
另外可以加入「前端下午茶交流群」微信群,長按識別下面二維碼即可加我好友,備注加群,我拉你入群~
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/92020.html
摘要:學習開發(fā),無論是前端開發(fā)還是都避免不了要接觸異步編程這個問題就和其它大多數(shù)以多線程同步為主的編程語言不同的主要設(shè)計是單線程異步模型。由于異步編程可以實現(xiàn)非阻塞的調(diào)用效果,引入異步編程自然就是順理成章的事情了。 學習js開發(fā),無論是前端開發(fā)還是node.js,都避免不了要接觸異步編程這個問題,就和其它大多數(shù)以多線程同步為主的編程語言不同,js的主要設(shè)計是單線程異步模型。正因為js天生的與...
摘要:異步本質(zhì)上應該就是多線程語言的產(chǎn)物。如果是多線程的異步,假死的應該是運行方法的線程,而方法仍然會按預期打印出。當然了,按我個人的理解,應該說是是的回調(diào)函數(shù)。 引子 每個故事都有由來。前兩天在看 gulp 的時候,看到了它有個 promise 的玩意兒,然后的然后,這兩天就掉進了 javascript 的異步和回調(diào)的坑里面去了。 其間搜索了 javascript promise,看到了...
摘要:圖片轉(zhuǎn)引自的演講和兩個定時器中回調(diào)的執(zhí)行邏輯便是典型的機制。異步編程關(guān)于異步編程我的理解是,在執(zhí)行環(huán)境所提供的異步機制之上,在應用編碼層面上實現(xiàn)整體流程控制的異步風格。 問題背景 在一次開發(fā)任務中,需要實現(xiàn)如下一個餅狀圖動畫,基于canvas進行繪圖,但由于對于JS運行環(huán)境中異步機制的不了解,所以遇到了一個棘手的問題,始終無法解決,之后在與同事交流之后才恍然大悟。問題的根節(jié)在于經(jīng)典的J...
摘要:今天的已經(jīng)成為一門功能全面的編程語言總結(jié)最初的用途是為來實現(xiàn)用戶與瀏覽器的交互二為何是單線程的的單線程,與它的用途有關(guān)。這決定了它只能是單線程,否則會帶來很復雜的同步問題。 showImg(https://user-gold-cdn.xitu.io/2019/3/31/169d1c40c27a173c?w=428&h=252&f=png&s=35393); 前言 我本來是打算寫一篇co...
摘要:從最開始的到封裝后的都在試圖解決異步編程過程中的問題。為了讓編程更美好,我們就需要引入來降低異步編程的復雜性。寫一個符合規(guī)范并可配合使用的寫一個符合規(guī)范并可配合使用的理解的工作原理采用回調(diào)函數(shù)來處理異步編程。 JavaScript怎么使用循環(huán)代替(異步)遞歸 問題描述 在開發(fā)過程中,遇到一個需求:在系統(tǒng)初始化時通過http獲取一個第三方服務器端的列表,第三方服務器提供了一個接口,可通過...
閱讀 3159·2021-10-14 09:42
閱讀 3564·2019-08-26 13:56
閱讀 3444·2019-08-26 11:59
閱讀 938·2019-08-23 18:00
閱讀 2196·2019-08-23 17:51
閱讀 3522·2019-08-23 17:17
閱讀 1479·2019-08-23 15:11
閱讀 5145·2019-08-23 15:05