如何解釋JavaScript的事件循環面試官才滿意?

觀感度:🌟🌟🌟🌟🌟

口味:甘梅地瓜

烹飪時間:30min

每天都在寫JavaScript的你,是否清楚JavaScript引擎的原理呢?

想要了解JavaScript引擎,首先我們從它的運行機制Event Loop來說起。

首先科普一些基礎知識。

進程和線程

進程

應用程序的執行實例,每一個進程都是由私有的虛擬地址空間、代碼、數據和其他系統資源所組成。

線程

線程是進程內的一個獨立執行單元,在不同的線程之間是可以共享進程資源的。

有句老話是這樣說的,窮養兒子富養女。

進程就是一個富二代爸爸,它選擇了窮養線程兒子。
21.jpg

進程擁有獨立的堆棧空間和數據段,每當啓動一個新的進程必須分配給它獨立的地址空間,建立衆多的數據表來維護它的代碼段、堆棧段和數據段。

線程擁有獨立的堆棧空間,但是共享數據段,它們彼此之間使用相同的地址空間,共享大部分數據,比進程更節儉,開銷比較小,切換速度也比進程快,效率高。

一句話解釋進程和線程

進程:資源分配的最小單位

線程:程序執行的最小單位

關於進程和線程方面的知識我們先瞭解到這,感興趣的同學們可以移步
進程和線程的區別

Q&A再來回答一個問題:

在多線程操作下可以實現應用的並行處理,從而以更高的 CPU 利用率提高整個應用程序的性能和吞吐量。特別是現在很多語言都支持多核並行處理技術,然而 JavaScript 卻以單線程執行,爲什麼呢?

答:JavaScript作爲腳本語言,最初被設計用於瀏覽器。爲了避免複雜的同步問題(做人嘛,還是簡單點好,語言也一樣),如果JavaScript同時有兩個線程,一個線程中執行在某個DOM節點上添加內容,另一個線程執行刪除這個節點,這時瀏覽器會……
22.jpg
所以JavaScript的單線程是這門語言的核心,未來也不會改變。

有人說,那HTML5的新特性Web Worker,可以創建多線程呀~

是的,爲了解決不可避免的耗時操作(多重循環、複雜的運算),HTML5提出了Web Worker,它會在當前的js執行主線程中開闢出一個額外的線程來運行js文件,這個新的線程和js主線程之間不會互相影響,同時提供了數據交換的接口:postMessageonMessage

但是因爲它創建的子線程完全受控於主線程,且位於外部文件中,無法訪問DOM。所以它並沒有改變js單線程的本質。

單線程就意味着,所有的任務都需要排隊。

就像還不能自助點餐的時候你去肯德基需要排隊,有的人沒想好點什麼或者點的東西很多,耗時就會長,那麼後面的人也只好排隊等待。有了自助點餐服務後,一切問題迎刃而解。

23.jpg

語言的設計和生活中的現實情況很像,IO設備(輸入輸出)很慢(比如Ajax),那麼語言的設計者意識到這一點,就在主線程中掛起處於等待中的任務,先運行後面的任務,等IO設備有了結果,再把掛起的任務執行下去。

Event Loop

24.jpg

從上圖中我們可以看到,在主線程運行時,會產生堆(heap)和棧(stack)。

堆中存的是我們聲明的object類型的數據,棧中存的是基本數據類型以及函數執行時的運行空間。

棧中的代碼會調用各種外部API,它們在任務隊列中加入各種事件(onClick,onLoad,onDone),只要棧中的代碼執行完畢(js引擎存在monitoring process進程,會持續不斷的檢查主線程執行棧是否爲空),主線程就回去讀取任務隊列,在按順序執行這些事件對應的回調函數。

也就是說主線程從任務隊列中讀取事件,這個過程是循環不斷的,所以這種運行機制又成爲Event Loop(事件循環)。

同步任務和異步任務

我們可以將任務分爲同步任務和異步任務。

同步任務就是在主線程上排隊執行的任務,只能執行完一個再執行下一個。

異步任務則不進入主線程,而是先在event table中註冊函數,當滿足觸發條件後,纔可以進入任務隊列來執行。只有任務隊列通知主線程說,我這邊異步任務可以執行了,這個時候此任務纔會進入主線程執行。

舉個🌰

console.log(a);

setTimeout(
  function () {
      console.log(b);
  },1000)
  
console.log(c)  

// a
// c
// b

1.console.log(a)是同步任務,進入主線程執行,打印a。

2.setTimeout是異步任務,先被放入event table中註冊,1000ms之後進入任務隊列。

3.console.log(c)是同步任務,進入主線程執行,打印c。

當a,c被打印後,主線程去事件隊列中找到setTimeout裏的函數,並執行,打印b。

綜上所述,b最持久~(扯個🥚)

宏任務和微任務

本文的MacrotaskWHATWG 中叫taskMacrotask爲了便於理解,並沒有實際的出處。

同步任務和異步任務的劃分其實並不準確,準確的分類方式是宏任務(Macrotask)和微任務(Microtask)。

宏任務包括:script(整體代碼), setTimeout, setInterval, requestAnimationFrame, I/O,setImmediate

其中setImmediate只存在於Node中,requestAnimationFrame只存在於瀏覽器中。

微任務包括: Promise, Object.observe(已廢棄), MutationObserver(html5新特性),process.nextTick

其中process.nextTick只存在於Node中,MutationObserver只存在於瀏覽器中。

注意:

UI Rendering不屬於宏任務,也不屬於微任務,它是一個與微任務平行的一個操作步驟。

HTML規範文檔

這種分類的執行方式就是,執行一個宏任務,過程中遇到微任務時,將其放到微任務的事件隊列裏,當前宏任務執行完成後,會查看微任務的事件隊列,依次執行裏面的微任務。如果還有宏任務的話,再重新開啓宏任務……

25.jpg

再舉個🌰

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.首先執行script下的宏任務,遇到setTimeout,將其放入宏任務的隊列裏。

2.遇到Promisenew Promise直接執行,打印b。

3.遇到then方法,是微任務,將其放到微任務的隊列裏。

4.遇到console.log('d'),直接打印。

5.本輪宏任務執行完畢,查看微任務,發現then方法裏的函數,打印c。

6.本輪event loop全部完成。

7.下一輪循環,先執行宏任務,發現宏任務隊列中有一個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')
    })
})

33.jpg
好,不要慫,我們來逐步分析。

第一輪事件循環:

1.第一個宏任務(整體script)進入主線程,console.log('a'),打印a。

2.遇到setTimeout,其回調函數進入宏任務隊列,暫定義爲setTimeout1

3.遇到process.nextTick(),其回調函數被分發到微任務隊列,暫定義爲process1

4.遇到Promisenew Promise直接執行,打印g。then進入微任務隊列,暫定義爲then1

5.遇到setTimeout,其回調函數進入宏任務隊列,暫定義爲setTimeout2

此時我們看一下兩個任務隊列中的情況

宏任務隊列 微任務隊列
setTimeout1、setTimeout2 process1、then1

第一輪宏任務執行完畢,打印出a和g。

查找微任務隊列中有process1then1。全部執行,打印f和h。

第一輪事件循環完畢,打印出a、g、f和h。

第二輪事件循環:

1.從setTimeout1宏任務開始,首先是console.lob('b'),打印b。

2.遇到process.nextTick(),進入微任務隊列,暫定義爲process2

3.new Promise直接執行,輸出d,then進入微任務隊列,暫定義爲then2

此時兩個任務隊列中

宏任務隊列 微任務隊列
setTimeout2 process2、then2

第二輪宏任務執行完畢,打印出b和d。

查找微任務隊列中有process2then2。全部執行,打印c和e。

第二輪事件循環完畢,打印出b、d、c和e。

第三輪事件循環

1.執行setTimeout2,遇到console.log('i'),打印i。

2.遇到process.nextTick(),進入微任務隊列,暫定義爲process3

3.new Promise直接執行,打印k。

4.then進入微任務隊列,暫定義爲then3

此時兩個任務隊列中

宏任務隊列:空

微任務隊列:process3then3

第三輪宏任務執行完畢,打印出i和k。

查找微任務隊列中有process3then3。全部執行,打印j和l。

第三輪事件循環完畢,打印出i、k、j和l。

到此爲止,三輪事件循環完畢,最終輸出結果爲:

a、g、f、h、b、d、c、e、i、k、j、l

l最持久,你答對了嗎?

以上代碼僅在瀏覽器環境中執行順序如下,node環境下可能存在不同。

看完本文希望你能夠理解JavaScript引擎的Event Loop執行機制,不僅可以讓我們更加深刻的認識JavaScript這門語言,而且面試被問起的時候可以和面試官侃侃而談。

交流

歡迎來我的個人公衆號交流,優質原創文章將同步推送。後臺回覆福利,即可領取福利,你懂得~

你的前端食堂,記得按時吃飯。
gongzhonghao.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章