如何解釋Event Loop面試官才滿意?
每天都在寫JavaScript的你,是否清楚JavaScript引擎的原理呢?
想要了解JavaScript引擎,首先我們從它的運行機制Event Loop來說起。
首先科普一些基礎(chǔ)知識。
進程和線程
進程
應(yīng)用程序的執(zhí)行實例,每一個進程都是由私有的虛擬地址空間、代碼、數(shù)據(jù)和其他系統(tǒng)資源所組成。
線程
線程是進程內(nèi)的一個獨立執(zhí)行單元,在不同的線程之間是可以共享進程資源的。
有句老話是這樣說的,窮養(yǎng)兒子富養(yǎng)女。
進程就是一個富二代爸爸,它選擇了窮養(yǎng)線程兒子。

進程擁有獨立的堆??臻g和數(shù)據(jù)段,每當啟動一個新的進程必須分配給它獨立的地址空間,建立眾多的數(shù)據(jù)表來維護它的代碼段、堆棧段和數(shù)據(jù)段。
線程擁有獨立的堆??臻g,但是共享數(shù)據(jù)段,它們彼此之間使用相同的地址空間,共享大部分數(shù)據(jù),比進程更節(jié)儉,開銷比較小,切換速度也比進程快,效率高。
一句話解釋進程和線程
進程:資源分配的最小單位
線程:程序執(zhí)行的最小單位
關(guān)于進程和線程方面的知識我們先了解到這,感興趣的同學們可以移步
https://www.cnblogs.com/Jones-dd/p/8858995.html
Q&A再來回答一個問題:
在多線程操作下可以實現(xiàn)應(yīng)用的并行處理,從而以更高的 CPU 利用率提高整個應(yīng)用程序的性能和吞吐量。特別是現(xiàn)在很多語言都支持多核并行處理技術(shù),然而 JavaScript 卻以單線程執(zhí)行,為什么呢?
答:JavaScript作為腳本語言,最初被設(shè)計用于瀏覽器。為了避免復(fù)雜的同步問題(做人嘛,還是簡單點好,語言也一樣),如果JavaScript同時有兩個線程,一個線程中執(zhí)行在某個DOM節(jié)點上添加內(nèi)容,另一個線程執(zhí)行刪除這個節(jié)點,這時瀏覽器會……

所以JavaScript的單線程是這門語言的核心,未來也不會改變。?
有人說,那HTML5的新特性Web Worker,可以創(chuàng)建多線程呀~
是的,為了解決不可避免的耗時操作(多重循環(huán)、復(fù)雜的運算),HTML5提出了Web Worker,它會在當前的js執(zhí)行主線程中開辟出一個額外的線程來運行js文件,這個新的線程和js主線程之間不會互相影響,同時提供了數(shù)據(jù)交換的接口:postMessage和onMessage。
但是因為它創(chuàng)建的子線程完全受控于主線程,且位于外部文件中,無法訪問DOM。所以它并沒有改變js單線程的本質(zhì)。
單線程就意味著,所有的任務(wù)都需要排隊。
就像還不能自助點餐的時候你去肯德基需要排隊,有的人沒想好點什么或者點的東西很多,耗時就會長,那么后面的人也只好排隊等待。有了自助點餐服務(wù)后,一切問題迎刃而解。

語言的設(shè)計和生活中的現(xiàn)實情況很像,IO設(shè)備(輸入輸出)很慢(比如Ajax),那么語言的設(shè)計者意識到這一點,就在主線程中掛起處于等待中的任務(wù),先運行后面的任務(wù),等IO設(shè)備有了結(jié)果,再把掛起的任務(wù)執(zhí)行下去。
Event Loop?

從上圖中我們可以看到,在主線程運行時,會產(chǎn)生堆(heap)和棧(stack)。
堆中存的是我們聲明的object類型的數(shù)據(jù),棧中存的是基本數(shù)據(jù)類型以及函數(shù)執(zhí)行時的運行空間。
棧中的代碼會調(diào)用各種外部API,它們在任務(wù)隊列中加入各種事件(onClick,onLoad,onDone),只要棧中的代碼執(zhí)行完畢(js引擎存在monitoring process進程,會持續(xù)不斷的檢查主線程執(zhí)行棧是否為空),主線程就回去讀取任務(wù)隊列,在按順序執(zhí)行這些事件對應(yīng)的回調(diào)函數(shù)。
也就是說主線程從任務(wù)隊列中讀取事件,這個過程是循環(huán)不斷的,所以這種運行機制又成為Event Loop(事件循環(huán))。
同步任務(wù)和異步任務(wù)
我們可以將任務(wù)分為同步任務(wù)和異步任務(wù)。
同步任務(wù)就是在主線程上排隊執(zhí)行的任務(wù),只能執(zhí)行完一個再執(zhí)行下一個。
異步任務(wù)則不進入主線程,而是先在event table中注冊函數(shù),當滿足觸發(fā)條件后,才可以進入任務(wù)隊列來執(zhí)行。只有任務(wù)隊列通知主線程說,我這邊異步任務(wù)可以執(zhí)行了,這個時候此任務(wù)才會進入主線程執(zhí)行。
舉個??
console.log(a);setTimeout(function () {console.log(b);},1000)console.log(c)// a// b// c
1.console.log(a)是同步任務(wù),進入主線程執(zhí)行,打印a。
2.setTimeout是異步任務(wù),先被放入event table中注冊,1000ms之后進入任務(wù)隊列。
3.console.log(c)是同步任務(wù),進入主線程執(zhí)行,打印c。
當a,c被打印后,主線程去事件隊列中找到setTimeout里的函數(shù),并執(zhí)行,打印b。
綜上所述,b最持久~(扯個??)
宏任務(wù)和微任務(wù)
同步任務(wù)和異步任務(wù)的劃分其實并不準確,準確的分類方式是宏任務(wù)(Macrotask)和微任務(wù)(Microtask)。
宏任務(wù)包括:
script(整體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。
微任務(wù)包括:
process.nextTick, Promise,
Object.observe(已廢棄), MutationObserver(html5新特性)。
這種分類的執(zhí)行方式就是,執(zhí)行一個宏任務(wù),過程中遇到微任務(wù)時,將其放到微任務(wù)的事件隊列里,當前宏任務(wù)執(zhí)行完成后,會查看微任務(wù)的事件隊列,依次執(zhí)行里面的微任務(wù)。如果還有宏任務(wù)的話,再重新開啟宏任務(wù)……

再舉個??
setTimeout(function() {console.log('a')});new Promise(function(resolve) {console.log('b');for(var i =0; i <10000; i++) {i ==99 && resolve();}}).then(function() {console.log('c')});console.log('d');// b// d// c// a
1.首先執(zhí)行script下的宏任務(wù),遇到setTimeout,將其放入宏任務(wù)的隊列里。
2.遇到Promise,new Promise直接執(zhí)行,打印b。
3.遇到then方法,是微任務(wù),將其放到微任務(wù)的隊列里。
4.遇到console.log('d'),直接打印。
5.本輪宏任務(wù)執(zhí)行完畢,查看微任務(wù),發(fā)現(xiàn)then方法里的函數(shù),打印c。
6.本輪event loop全部完成。
7.下一輪循環(huán),先執(zhí)行宏任務(wù),發(fā)現(xiàn)宏任務(wù)隊列中有一個setTimeout,打印a。
綜上所述,不要說a是最持久的,如果你認為你徹底明白了,給你出道題,看看下面的代碼中,誰最持久?
console.log('a');setTimeout(function() {console.log('b');process.nextTick(function() {console.log('c');})new Promise(function(resolve) {console.log('d');resolve();}).then(function() {console.log('e')})})process.nextTick(function() {console.log('f');})new Promise(function(resolve) {console.log('g');resolve();}).then(function() {console.log('h')})setTimeout(function() {console.log('i');process.nextTick(function() {console.log('j');})new Promise(function(resolve) {console.log('k');resolve();}).then(function() {console.log('l')})})

好,不要慫,我們來逐步分析。
第一輪事件循環(huán):
1.第一個宏任務(wù)(整體script)進入主線程,console.log('a'),打印a。
2.遇到setTimeout,其回調(diào)函數(shù)進入宏任務(wù)隊列,暫定義為setTimeout1。
3.遇到process.nextTick(),其回調(diào)函數(shù)被分發(fā)到微任務(wù)隊列,暫定義為process1。
4.遇到Promise,new Promise直接執(zhí)行,打印g。then進入微任務(wù)隊列,暫定義為then1。
5.遇到setTimeout,其回調(diào)函數(shù)進入宏任務(wù)隊列,暫定義為setTimeout2。
此時我們看一下兩個任務(wù)隊列中的情況:
宏任務(wù)隊列:setTimeout1、setTimeout2
微任務(wù)隊列:process1、then1
第一輪宏任務(wù)執(zhí)行完畢,打印出a和g。
查找微任務(wù)隊列中有process1和then1。全部執(zhí)行,打印f和h。
第一輪事件循環(huán)完畢,打印出a、g、f和h。
第二輪事件循環(huán):
1.從setTimeout1宏任務(wù)開始,首先是console.lob('b'),打印b。
2.遇到process.nextTick(),進入微任務(wù)隊列,暫定義為process2。
3.new Promise直接執(zhí)行,輸出d,then進入微任務(wù)隊列,暫定義為then2。
此時兩個任務(wù)隊列中
宏任務(wù)隊列:setTimeout2
微任務(wù)隊列:process2、 then2
第二輪宏任務(wù)執(zhí)行完畢,打印出b和d。
查找微任務(wù)隊列中有process2和then2。全部執(zhí)行,打印c和e。
第二輪事件循環(huán)完畢,打印出b、d、c和e。
第三輪事件循環(huán):
1.執(zhí)行setTimeout2,遇到console.log('i'),打印i。
2.遇到process.nextTick(),進入微任務(wù)隊列,暫定義為process3。
3.new Promise直接執(zhí)行,打印k。
4.then進入微任務(wù)隊列,暫定義為then3。
此時兩個任務(wù)隊列中
宏任務(wù)隊列:空
微任務(wù)隊列:process3、then3
第三輪宏任務(wù)執(zhí)行完畢,打印出i和k。
查找微任務(wù)隊列中有process3和then3。全部執(zhí)行,打印j和l。
第三輪事件循環(huán)完畢,打印出i、k、j和l。
到此為止,三輪事件循環(huán)完畢,最終輸出結(jié)果為:
a、g、f、h、b、d、c、e、i、k、j、l
l最持久,你答對了嗎?
以上代碼僅在瀏覽器環(huán)境中執(zhí)行順序如下,node環(huán)境下可能存在不同。
看完本文希望你能夠理解JavaScript引擎的Event Loop執(zhí)行機制,不僅可以讓我們更加深刻的認識JavaScript這門語言,而且面試被問起的時候可以和面試官侃侃而談。
如果覺得看完本文有所收獲,請點擊右下角在看支持,感謝支持。
