《進擊的前端工程師》系列-瀏覽器中JavaScript的事件循環

觀感度:🌟🌟🌟🌟🌟javascript

口味:甘梅地瓜html

烹飪時間:30min前端




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

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

首先科普一些基礎知識。
java

進程和線程

進程node

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

線程面試

線程是進程內的一個獨立執行單元,在不一樣的線程之間是能夠共享進程資源的。api

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

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

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

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

一句話解釋進程和線程

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

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

關於進程和線程方面的知識咱們先了解到這,感興趣的同窗們能夠移步 進程和線程的區別

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

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

答:JavaScript做爲腳本語言,最初被設計用於瀏覽器。爲了不復雜的同步問題(作人嘛,仍是簡單點好,語言也同樣),若是JavaScript同時有兩個線程,一個線程中執行在某個DOM節點上添加內容,另外一個線程執行刪除這個節點,這時瀏覽器會……

因此JavaScript的單線程是這門語言的核心,將來也不會改變。

有人說,那HTML5的新特性Web Worker,能夠建立多線程呀~

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

可是由於它建立的子線程徹底受控於主線程,且位於外部文件中,沒法訪問DOM。因此它並無改變js單線程的本質。

單線程就意味着,全部的任務都須要排隊。

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

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

Event Loop

從上圖中咱們能夠看到,在主線程運行時,會產生堆(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規範文檔

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

再舉個🌰

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')
    })
})
複製代碼

好,不要慫,咱們來逐步分析。

第一輪事件循環:

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

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

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

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

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

此時咱們看一下兩個任務隊列中的狀況

宏任務隊列 微任務隊列
setTimeout一、setTimeout2 process一、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 process二、 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這門語言,並且面試被問起的時候能夠和麪試官侃侃而談。

交流

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

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

相關文章
相關標籤/搜索