深入分析 Javascript 单线程
面試的時(shí)候發(fā)現(xiàn)99%的童鞋不理解為什么JavaScript是單線程的卻能讓AJAX異步發(fā)送和回調(diào)請(qǐng)求,還有setTimeout也看起來(lái)像是多線程的?還有non-blocking IO, event loop等概念很不清楚。來(lái)深入分析一下:
首先看下面的代碼:
| 1 2 3 4 5 6 7 8 9 | function?foo() { ????console.log( 'first'?); ????setTimeout( ( function(){ console.log( 'second'?); } ), 5); } for?(var?i = 0; i < 1000000; i++) { ????foo(); } |
執(zhí)行結(jié)果會(huì)首先全部輸出first,然后全部輸出second;盡管中間的執(zhí)行會(huì)超過(guò)5ms。為什么?
Javascript是單線程的
因?yàn)?strong>JS運(yùn)行在瀏覽器中,是單線程的,每個(gè)window一個(gè)JS線程,既然是單線程的,在某個(gè)特定的時(shí)刻只有特定的代碼能夠被執(zhí)行,并阻塞其它的代碼。而瀏覽器是事件驅(qū)動(dòng)的(Event driven),瀏覽器中很多行為是異步(Asynchronized)的,會(huì)創(chuàng)建事件并放入執(zhí)行隊(duì)列中。javascript引擎是單線程處理它的任務(wù)隊(duì)列,你可以理解成就是普通函數(shù)和回調(diào)函數(shù)構(gòu)成的隊(duì)列。當(dāng)異步事件發(fā)生時(shí),如mouse click, a timer firing, or an XMLHttpRequest completing(鼠標(biāo)點(diǎn)擊事件發(fā)生、定時(shí)器觸發(fā)事件發(fā)生、XMLHttpRequest完成回調(diào)觸發(fā)等),將他們放入執(zhí)行隊(duì)列,等待當(dāng)前代碼執(zhí)行完成。
異步事件驅(qū)動(dòng)
前面已經(jīng)提到瀏覽器是事件驅(qū)動(dòng)的(Event driven),瀏覽器中很多行為是異步(Asynchronized)的,例如:鼠標(biāo)點(diǎn)擊事件、窗口大小拖拉事件、定時(shí)器觸發(fā)事件、XMLHttpRequest完成回調(diào)等。當(dāng)一個(gè)異步事件發(fā)生的時(shí)候,它就進(jìn)入事件隊(duì)列。瀏覽器有一個(gè)內(nèi)部大消息循環(huán),Event Loop(事件循環(huán)),會(huì)輪詢大的事件隊(duì)列并處理事件。例如,瀏覽器當(dāng)前正在忙于處理onclick事件,這時(shí)另外一個(gè)事件發(fā)生了(如:window onSize),這個(gè)異步事件就被放入事件隊(duì)列等待處理,只有前面的處理完畢了,空閑了才會(huì)執(zhí)行這個(gè)事件。setTimeout也是一樣,當(dāng)調(diào)用的時(shí)候,js引擎會(huì)啟動(dòng)定時(shí)器timer,大約xxms以后執(zhí)行xxx,當(dāng)定時(shí)器時(shí)間到,就把該事件放到主事件隊(duì)列等待處理(瀏覽器不忙的時(shí)候才會(huì)真正執(zhí)行)。
每個(gè)瀏覽器具體實(shí)現(xiàn)主事件隊(duì)列不盡相同,這不談了。
瀏覽器不是單線程的
雖然JS運(yùn)行在瀏覽器中,是單線程的,每個(gè)window一個(gè)JS線程,但瀏覽器不是單線程的,例如Webkit或是Gecko引擎,都可能有如下線程:
- javascript引擎線程
- 界面渲染線程
- 瀏覽器事件觸發(fā)線程
- Http請(qǐng)求線程
很多童鞋搞不清,如果js是單線程的,那么誰(shuí)去輪詢大的Event loop事件隊(duì)列?答案是瀏覽器會(huì)有單獨(dú)的線程去處理這個(gè)隊(duì)列。
Ajax異步請(qǐng)求是否真的異步?
很多童鞋搞不清楚,既然說(shuō)JavaScript是單線程運(yùn)行的,那么XMLHttpRequest在連接后是否真的異步??
其實(shí)請(qǐng)求確實(shí)是異步的,這請(qǐng)求是由瀏覽器新開一個(gè)線程請(qǐng)求(見前面的瀏覽器多線程)。當(dāng)請(qǐng)求的狀態(tài)變更時(shí),如果先前已設(shè)置回調(diào),這異步線程就產(chǎn)生狀態(tài)變更事件放到 JavaScript引擎的事件處理隊(duì)列中等待處理。當(dāng)瀏覽器空閑的時(shí)候出隊(duì)列任務(wù)被處理,JavaScript引擎始終是單線程運(yùn)行回調(diào)函數(shù)。javascript引擎確實(shí)是單線程處理它的任務(wù)隊(duì)列,能理解成就是普通函數(shù)和回調(diào)函數(shù)構(gòu)成的隊(duì)列。
總結(jié)一下,Ajax請(qǐng)求確實(shí)是異步的,這請(qǐng)求是由瀏覽器新開一個(gè)線程請(qǐng)求,事件回調(diào)的時(shí)候是放入Event loop單線程事件隊(duì)列等候處理。
setTimeout(func, 0)為什么有時(shí)候有用?
寫js多的童鞋可能發(fā)現(xiàn),有時(shí)候加一個(gè)setTimeout(func, 0)非常有用,為什么?難道是模擬多線程嗎?錯(cuò)!前面已經(jīng)說(shuō)過(guò)了,javascript是JS運(yùn)行在瀏覽器中,是單線程的,每個(gè)window一個(gè)JS線程,既然是單線程的,setTimeout(func, 0)神奇在哪兒?那就是告訴js引擎,在0ms以后把func放到主事件隊(duì)列中,等待當(dāng)前的代碼執(zhí)行完畢再執(zhí)行,注意:重點(diǎn)是改變了代碼流程,把func的執(zhí)行放到了等待當(dāng)前的代碼執(zhí)行完畢再執(zhí)行。這就是它的神奇之處了。它的用處有三個(gè):
- 讓瀏覽器渲染當(dāng)前的變化(很多瀏覽器UI render和js執(zhí)行是放在一個(gè)線程中,線程阻塞會(huì)導(dǎo)致界面無(wú)法更新渲染)
- 重新評(píng)估”script is running too long”警告
- 改變執(zhí)行順序
例如:下面的例子,點(diǎn)擊按鈕就會(huì)顯示"calculating....",如果刪除setTimeout就不會(huì)。因?yàn)閞eDraw事件被進(jìn)入事件隊(duì)列到長(zhǎng)時(shí)間操作的最后才能被執(zhí)行,所以無(wú)法刷新。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | <button id='do'> Do long calc!</button> <div id='status'></div> <div id='result'></div> $('#do').on('click', function(){ ??$('#status').text('calculating....'); //此處會(huì)觸發(fā)redraw事件的fired,但會(huì)放到隊(duì)列里執(zhí)行,直到long()執(zhí)行完。 ??// without set timeout, user will never see "calculating...." ??//long();//執(zhí)行長(zhǎng)時(shí)間任務(wù),阻塞 ??// with set timeout, works as expected ??setTimeout(long,50);//用定時(shí)器,大約50ms以后執(zhí)行長(zhǎng)時(shí)間任務(wù),放入執(zhí)行隊(duì)列,但在redraw之后了,根據(jù)先進(jìn)先出原則 ?}) function?long(){ ??var?result = 0 ??for?(var?i = 0; i<1000; i++){ ????for?(var?j = 0; j<1000; j++){ ??????for?(var?k = 0; k<1000; k++){ ????????result = result + i+j+k ??????} ????} ??} ??$('#status').text('calclation done') // has to be in here for this example. or else it will ALWAYS run instantly. This is the same as passing it a callback } |
非阻塞js的實(shí)現(xiàn)(non-blocking javascript)
js在瀏覽器中需要被下載、解釋并執(zhí)行這三步。在html body標(biāo)簽中的script都是阻塞的。也就是說(shuō),順序下載、解釋、執(zhí)行。盡管Chrome可以實(shí)現(xiàn)多線程并行下載外部資源,例如:script file、image、frame等(css比較復(fù)雜,在IE中不阻塞下載,但Firefox阻塞下載)。但是,由于js是單線程的,所以盡管瀏覽器可以并發(fā)加快js的下載,但必須依次執(zhí)行。所以chrome中image圖片資源是可以并發(fā)下載的,但外部js文件并發(fā)下載沒(méi)有多大意義。
要實(shí)現(xiàn)非阻塞js(non-blocking javascript)有兩個(gè)方法:1. html5 2. 動(dòng)態(tài)加載js
首先一種辦法是HTML5的defer和async關(guān)鍵字:
defer
| 1 | <script type="text/javascript"?defer src="foo.js"></script> |
async
| 1 | <script type="text/javascript"?async src="foo.js"></script> |
然后第二種方法是動(dòng)態(tài)加載js:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | setTimeout(function(){ ????var?script = document.createElement("script"); ????script.type = "text/javascript"; ????script.src = "foo.js"; ????var?head = true; //加在頭還是尾 ????if(head) ??????document.getElementsByTagName("head")[0].appendChild(script); ????else ??????document.body.appendChild(script); }, 0); //另外一個(gè)獨(dú)立的動(dòng)態(tài)加載js的函數(shù) function?loadJs(jsurl, head, callback){ ????var?script=document.createElement('script'); ????script.setAttribute("type","text/javascript"); ????if(callback){ ????????if?(script.readyState){? //IE ????????????script.onreadystatechange = function(){ ????????????????if?(script.readyState == "loaded"?|| ????????????????????????script.readyState == "complete"){ ????????????????????script.onreadystatechange = null; ????????????????????callback(); ????????????????} ????????????}; ????????} else?{? //Others ????????????script.onload = function(){ ????????????????callback(); ????????????}; ????????} ????} ????script.setAttribute("src", jsurl); ????if(head) ?????document.getElementsByTagName('head')[0].appendChild(script); ????else ??????document.body.appendChild(script); } |
總結(jié)
以上是生活随笔為你收集整理的深入分析 Javascript 单线程的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 移动web app开发必备 - 异步队列
- 下一篇: 几本推荐的Java书