从Javascript单线程谈Event Loop
假如面試回答js的運(yùn)行機(jī)制時(shí),你可能說出這么一段話:“Javascript的事件分同步任務(wù)和異步任務(wù),遇到同步任務(wù)就放在執(zhí)行棧中執(zhí)行,而碰到異步任務(wù)就放到任務(wù)隊(duì)列之中,等到執(zhí)行棧執(zhí)行完畢之后再去執(zhí)行任務(wù)隊(duì)列之中的事件。”但你能說出背后的原因嗎?
?
先理解相關(guān)概念
線程與進(jìn)程
進(jìn)程:是系統(tǒng)資源分配和調(diào)度的單元。一個(gè)運(yùn)行著的程序就對(duì)應(yīng)了一個(gè)進(jìn)程。一個(gè)進(jìn)程包括了運(yùn)行中的程序和程序所使用到的內(nèi)存和系統(tǒng)資源。
線程:線程是進(jìn)程下的執(zhí)行者,一個(gè)進(jìn)程至少會(huì)開啟一個(gè)線程(主線程),也可以開啟多個(gè)線程。
?
同步和異步
同步和異步關(guān)注的是:消息(結(jié)果)通信機(jī)制
同步:發(fā)出調(diào)用后,在沒有得到結(jié)果前,該調(diào)用不返回。但是一旦調(diào)用返回,就得到返回值
異步:發(fā)出調(diào)用后,調(diào)用直接返回,沒有返回結(jié)果。但結(jié)果由回調(diào)函數(shù)給出,至于什么時(shí)候給出,不知道。(這個(gè)回調(diào)函數(shù)肯定是在當(dāng)前js執(zhí)行完后才執(zhí)行)
?
阻塞與非阻塞
阻塞和非阻塞關(guān)注的是:程序在等待調(diào)用結(jié)果時(shí)的狀態(tài).
阻塞調(diào)用:調(diào)用結(jié)果返回之前,當(dāng)前線程被掛起。調(diào)用線程只有在得到結(jié)果后才會(huì)返回。
非阻塞調(diào)用:在不能立刻得到結(jié)果之前,該調(diào)用不會(huì)阻塞當(dāng)前線程。
?
為什么JavaScript是單線程?
JavaScript是單線程,程序按照順序排列,前面的必須處理好,后面的才會(huì)執(zhí)行。JavaScript的設(shè)計(jì)初衷是作為瀏覽器腳本語言,主要是簡單用戶交互、操作DOM等,所以這門語言要圍繞單線程來設(shè)計(jì),否則出現(xiàn)復(fù)雜的同步問題。
由于JavaScript是單線程的,對(duì)于耗時(shí)的或者時(shí)間不確定的操作,我們可以使用異步編程,因?yàn)?span style="font-family:'宋體';">異步可以實(shí)現(xiàn)非阻塞操作。當(dāng)然也可以用HTML5標(biāo)準(zhǔn)的Web Worker。本文不作討論,詳細(xì)參考MDN文檔:點(diǎn)擊這里
既然js是單線程執(zhí)行的,那誰去輪詢大的任務(wù)隊(duì)列?這不矛盾了嗎?
?
Js的單線程與異步矛盾嗎?
不矛盾!!!首先記住這句話:Js執(zhí)行是單線程,但瀏覽器是多線程。
1)JS的單線程
一個(gè)瀏覽器進(jìn)程中只有一個(gè)JS的執(zhí)行線程,同一時(shí)刻內(nèi)只會(huì)有一段代碼在執(zhí)行
?
2)瀏覽器是多線程
查閱資料,有些文章也說是模塊,本文就以瀏覽器是多線程來說,它有以下常駐線程:
渲染引擎線程:負(fù)責(zé)頁面的渲染
JS引擎線程:負(fù)責(zé)JS的解析和執(zhí)行(本文說的主線程就指js引擎線程)
定時(shí)器觸發(fā)線程:處理定時(shí)事件,比如setTimeout, setInterval
事件觸發(fā)線程:處理DOM事件
異步http請(qǐng)求線程:處理http請(qǐng)求
? ? ? ? ......
瀏覽器是Js的使用場景,瀏覽器本身是典型的 GUI 工作線程(GUI 工作線程在絕大多數(shù)系統(tǒng)中都實(shí)現(xiàn)為事件處理,避免阻塞交互)。故瀏覽器是事件驅(qū)動(dòng)的(Event driven),瀏覽器中很多行為是異步,會(huì)創(chuàng)建事件并放入任務(wù)隊(duì)列中。
由于Javascript 是單線程,它需要借助異步完成耗時(shí)或者時(shí)間不確定的操作,這些操作由瀏覽器的其它線程執(zhí)行,這形成了異步事件驅(qū)動(dòng)。異步事件驅(qū)動(dòng)往往由瀏覽器的兩個(gè)或以上常駐線程共同完成的。例如ajax異步請(qǐng)求是由JS執(zhí)行線程和異步http請(qǐng)求線程,事件觸發(fā)線程共同完成的。
?
事件循環(huán)機(jī)制(Event Loop)
相關(guān)概念
棧
函數(shù)調(diào)用形成一個(gè)棧幀。
1 function foo(b) { 2 let a = 10; 3 return a + b + 11; 4 } 5 6 function bar(x) { 7 let y = 3; 8 return foo(x * y); 9 } 10 11 console.log(bar(7));當(dāng)調(diào)用 bar?時(shí),創(chuàng)建了第一個(gè)幀 ,幀中包含了 bar?的參數(shù)和局部變量。
當(dāng) bar?調(diào)用 foo?時(shí),第二個(gè)幀就被創(chuàng)建,并被壓到第一個(gè)幀之上,幀中包含了 foo 的參數(shù)和局部變量。
當(dāng) foo 返回時(shí),最上層的幀就被彈出棧(剩下 bar?函數(shù)的調(diào)用幀 )。
當(dāng) bar?返回的時(shí)候,棧就空了。
堆
對(duì)象被分配在一個(gè)堆中,一個(gè)用以表示一個(gè)內(nèi)存中大的未被組織的區(qū)域。
每一個(gè)線程只有一個(gè)棧,每一個(gè)程序只有一個(gè)堆。
隊(duì)列
一個(gè) JavaScript 運(yùn)行時(shí)包含了一個(gè)待處理的消息隊(duì)列。每一個(gè)消息都與一個(gè)函數(shù)相關(guān)聯(lián)。
當(dāng)棧為空時(shí),從隊(duì)列中取出一個(gè)消息進(jìn)行處理。這個(gè)處理過程包含了調(diào)用與這個(gè)消息相關(guān)聯(lián)的函數(shù)。
當(dāng)棧再次為空的時(shí)候,也就意味著消息處理結(jié)束。
?
任務(wù)隊(duì)列(消息隊(duì)列)
任務(wù)隊(duì)列是一個(gè)先進(jìn)先出的數(shù)據(jù)結(jié)構(gòu),當(dāng)主線程執(zhí)行棧一清空,任務(wù)隊(duì)列的回調(diào)函數(shù)就自動(dòng)進(jìn)入主線程。任務(wù)分成兩種:
1、同步任務(wù):在主線程上排隊(duì)執(zhí)行的任務(wù)。只有執(zhí)行完當(dāng)前任務(wù),才能執(zhí)行后一個(gè)任務(wù)。
2、異步任務(wù):該任務(wù)不進(jìn)入主線程、而進(jìn)入任務(wù)隊(duì)列。當(dāng)執(zhí)行棧清空后,才去執(zhí)行任務(wù)隊(duì)列中的任務(wù)。
?
異步執(zhí)行的運(yùn)行機(jī)制
由于JavaScript只能一次執(zhí)行一段代碼(由于其單線程性質(zhì)),這些代碼塊中的每一個(gè)都“阻止”其他異步事件的進(jìn)度。這意味著當(dāng)異步事件發(fā)生時(shí)(如鼠標(biāo)點(diǎn)擊,定時(shí)器觸發(fā)或XMLHttpRequest完成),它將排隊(duì)等待稍后執(zhí)行(這種排隊(duì)實(shí)際發(fā)生的確定會(huì)因?yàn)g覽器到瀏覽器而異)。
1、所有同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)執(zhí)行棧。
2、當(dāng)遇到異步任務(wù)時(shí)(IO設(shè)備操作等),就在任務(wù)隊(duì)列中添加一個(gè)事件,這個(gè)事件對(duì)應(yīng)著該異步任務(wù)的回調(diào)函數(shù)。
3、執(zhí)行棧中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會(huì)讀取任務(wù)隊(duì)列,進(jìn)入執(zhí)行棧,開始執(zhí)行。
4、主線程不斷重復(fù)第三步。這就形成了事件循環(huán)
結(jié)論:Javascript的事件分同步任務(wù)和異步任務(wù),遇到同步任務(wù)就放在執(zhí)行棧中執(zhí)行,而碰到異步任務(wù)就放到任務(wù)隊(duì)列之中,等到執(zhí)行棧執(zhí)行完畢之后再去執(zhí)行任務(wù)隊(duì)列之中的事件。
?
事件和回調(diào)函數(shù)的概念必要說明
- 工作線程:是本文對(duì)除了js引擎線程之外的其它線程的統(tǒng)稱
- 回調(diào)函數(shù):在一個(gè)函數(shù)中調(diào)用另外一個(gè)函數(shù)。這里指異步場景下為了非阻塞那些被主線程掛起來的代碼。
- ?主線程讀取任務(wù)隊(duì)列,就是讀取里面有哪些事件,執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)。
工作線程完成一項(xiàng)任務(wù),就向任務(wù)隊(duì)列中添加一個(gè)事件。這里的完成任務(wù)是指完成操作(click、mouse、touch,ajax的數(shù)據(jù)完全請(qǐng)求回來......),并非指執(zhí)行它的回調(diào)函數(shù)
a.onclick = function () {console.log("roro") }如上段代碼,僅是操作了click,但并沒有執(zhí)行回調(diào)函數(shù)打印roro
?
事件循環(huán)
事件循環(huán)是:主線程重復(fù)從任務(wù)隊(duì)列中取消息(事件),執(zhí)行對(duì)應(yīng)回調(diào)函數(shù)的過程。
?
?
上圖中,主線程運(yùn)行的時(shí)候,產(chǎn)生堆(heap)和棧(stack),棧中的代碼調(diào)用各種外部API,它們?cè)谌蝿?wù)隊(duì)列中加入各種事件(click,load,done)。只要執(zhí)行引擎棧棧中的代碼執(zhí)行完畢,主線程就會(huì)去讀取任務(wù)隊(duì)列,依次執(zhí)行那些事件所對(duì)應(yīng)的回調(diào)函數(shù)。
?
定時(shí)器
首先參考這篇外國人的文章:how-javascript-timers-work,定時(shí)器的執(zhí)行原理及細(xì)節(jié)。
setTimeout()怎么執(zhí)行?
setTimeout(function () {console.log('a'); }, 5000)? ? ? ?Javascript執(zhí)行引擎(主線程)運(yùn)行的時(shí)候,產(chǎn)生堆和棧。程序中代碼依次進(jìn)入棧中等待執(zhí)行,當(dāng)調(diào)用setTimeout()方法時(shí),在瀏覽器的定時(shí)器線程下處理延時(shí)方法,當(dāng)setTimeout方法執(zhí)行5秒后,到達(dá)觸發(fā)條件,方法被添加到用于回調(diào)的任務(wù)隊(duì)列。
? ? ? ?當(dāng)執(zhí)行引擎的執(zhí)行棧為空,執(zhí)行引擎開始輪詢檢查任務(wù)隊(duì)列是否有任務(wù)需要被執(zhí)行,當(dāng)檢查到已經(jīng)符合執(zhí)行條件的延時(shí)方法時(shí),將延時(shí)方法console.log('a')壓入執(zhí)行棧,引擎發(fā)現(xiàn)調(diào)用了log()方法,于是又將log()方法入棧。然后對(duì)執(zhí)行棧依次出棧執(zhí)行,輸出‘a(chǎn)’,清空?qǐng)?zhí)行棧,整個(gè)執(zhí)行完畢。
?
setTimeout(fn,0)是立即執(zhí)行嗎?
在javascript權(quán)威指南中:當(dāng)setTimeout的延遲時(shí)間設(shè)置為0的時(shí)候,回調(diào)函數(shù)不會(huì)馬上執(zhí)行,而是進(jìn)入?事件隊(duì)列。
?
btn.onclick = function () {setTimeout(function () {console.log('a')}, 0); }?
setTimeout(fn,0)的含義是:指定某個(gè)任務(wù)在主線程的空閑時(shí)間下,盡可能早地執(zhí)行。它被添加進(jìn)任務(wù)隊(duì)列,因此要等到同步任務(wù)和任務(wù)隊(duì)列中的前一個(gè)事件都處理完,才會(huì)執(zhí)行。
HTML5標(biāo)準(zhǔn)規(guī)定了setTimeout()的第二個(gè)參數(shù)的最小值不得低于4ms,如果低于這個(gè)值,就會(huì)自動(dòng)增加。老版本的瀏覽器允許最短間隔設(shè)為10ms。詳細(xì)參考MDN文檔:最小延遲和超時(shí)嵌套
所以setTimeout(fn,0)并不是立即執(zhí)行。假若你想實(shí)現(xiàn)0ms 的timeout可以用?window.postMessage(),本文不作討論。
?
兩道經(jīng)典的面試題:
一)以下代碼輸出什么?
function foo() {console.log('a')setTimeout(function () {console.log('b');}, 500) }for (let i = 0; i < 10000; i++) {foo() }執(zhí)行結(jié)果:首先全部輸出a,中間等待500ms,然后全部輸出b。上圖是個(gè)人理解,不恰當(dāng)?shù)牡胤秸?qǐng)指出!
?
二)ajax異步請(qǐng)求是否真的異步?
1、JS的執(zhí)行線程(主線程)發(fā)起異步請(qǐng)求,瀏覽器會(huì)開一條新的HTTP請(qǐng)求線程來執(zhí)行請(qǐng)求,繼續(xù)執(zhí)行棧中剩下的任務(wù),
2、在新線程(HTTP請(qǐng)求線程)中,在執(zhí)行請(qǐng)求的同時(shí),瀏覽器會(huì)正常處理其他任務(wù)的執(zhí)行。
3、在未來的某一時(shí)刻,當(dāng)數(shù)據(jù)完全請(qǐng)求回來以后,事件觸發(fā)線程監(jiān)視到之前發(fā)起的HTTP請(qǐng)求已完成,會(huì)將指定的回調(diào)函數(shù)放入任務(wù)隊(duì)列中。
4、當(dāng)瀏覽器執(zhí)行棧空閑時(shí),去掃描任務(wù)隊(duì)列中的回調(diào)函數(shù),依次壓入執(zhí)行棧中處理。
所以:ajax請(qǐng)求是異步。由瀏覽器新開一個(gè)線程請(qǐng)求,事件回調(diào)的時(shí)候放入Event loop任務(wù)隊(duì)列等候處理。詳細(xì)的例子,可以參考MDN文檔對(duì)ajax的描述:同步和異步
?
誤解:事件循環(huán)類似棧或隊(duì)列
這里順帶提一下:事件循環(huán)雖然涉及到類似隊(duì)列的結(jié)構(gòu),并不是采用棧的方式處理任務(wù)。事件循環(huán)作為一個(gè)進(jìn)程被劃分為多個(gè)階段,每個(gè)階段處理一些特定任務(wù),各階段輪詢調(diào)度。這些階段可以是定時(shí)器處理,dom事件處理,ajax異步處理......
?
結(jié)語
JavaScript引擎只有一個(gè)線程,強(qiáng)制異步事件排隊(duì)等待執(zhí)行,Javascript語言的事件循環(huán),是瀏覽器的處理和行為。另外,本文是我個(gè)人的學(xué)習(xí)筆記,通篇結(jié)合個(gè)人的理解,在某些地方表述不嚴(yán)謹(jǐn),如有錯(cuò)誤,希望指出。
轉(zhuǎn)載于:https://www.cnblogs.com/LIUYANZUO/p/7353547.html
總結(jié)
以上是生活随笔為你收集整理的从Javascript单线程谈Event Loop的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux的时区怎样设置
- 下一篇: 基于Java的Selenium学习笔记—