倒底如何解釋Event Loop

倒底如何解釋Event Loop

八月 18, 2021

開頭

首先我們從它的運行機制Event Loop來說起。

首先科普一些基礎知識。

程序(進程)、執行緒(線程)

進程 Process

Process 進程則是指被執行且載入記憶體的 program。Process 也是 OS 分配資源的最小單位,可以從 OS 得到如 CPU Time、Memory…等資源,意思是這個 process 在運行時會消耗多少 CPU 與記憶體。文章一開始放了一張 MacOS 活動監視器的截圖,相信不管是使用哪種作業系統的讀者都有看過類似的介面,而監視器中列出的是你的電腦正在執行的應用程式,而它們其實就是一個個 process。

線程 Thread

線程可以想像成存在於 process 裡面,而一個進程裡至少會有一個線程,前面有說 process 是 OS 分配資源的最小單位,而 thread 則是作業系統能夠進行運算排程的最小單位,也就是說實際執行任務的並不是進程,而是進程中的線程,一個進程有可能有多個線程,其中多個線程可以共用進程的系統資源。

一個問題

  • 在多線程操作下實現應用的並行處理,能夠以更高的CPU擴展提高整個程序的性能和語言處理能力都特別是現在,但JavaScript卻以單線程執行,為什麼呢?
  • 答:JavaScript作為腳本語言,最初是為了避免複雜的同步問題(做人嘛,還是簡單點好,也一樣),如果JavaScript同時有兩個線程,一個線程中執行在某個DOM節點上添加,另一個線程執行刪除這個節點,瀏覽器會……

一臉矇逼

所以 JavaScript 的單線程是這門語言的核心,未來也不會改變。

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

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

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


event loop

從上到下可以看到,在主線程運行時,會產生堆(堆)和棧(棧)。

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

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

到主線程從任務這個任務中讀取事件,所以這個過程是循環不斷的,所以這種運行機制又成為事件循環(事件循環)。

步任務和異步任務

我們將任務分為同步任務和異步任務。

同步任務就是在主線程上執行的任務,可以執行一個再執行下一個。

異步任務則不進入主線程,可能先在事件表中註冊函數,當滿足觸發條件後,可以進入任務召喚來執行。此任務將進入主線程執行。

  • 舉例
1
2
3
4
5
6
7
8
9
10
11
12
console.log(a);

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

console.log(c)

// a
// c
// b

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

2.setTimeout是異步任務,先被動態事件表中註冊,1000ms後進入任務探測。

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

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

宏任務和微任務

同步任務和異步任務的劃分細節宏觀,具體的分類方式是任務(Macrotask)和微任務(Microtask)。

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

設置立即只存在於節點中,requestAnimationFrame 只存在於瀏覽器中。

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

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

UI Rendering不屬於宏任務,也不屬於微任務,它是一個與微任務類似的一個操作步驟。
    https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model

這些分類的執行,執行一個宏任務,過程中遇到的微任務時,將其現在微當前的事件捕捉,執行完後任務中的任務,具體可以查看詳細任務的細節,具體執行內容的微任務。如果還有宏任務的話,再重新開啟宏任務……

task

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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.首先執行腳本下的宏任務,遇到setTimeout,將其原生宏任務的召喚裡。

  1. 遇到Promise,new Promise直接執行,印出b。

3.遇到然後方法,是微任務將其可以微任務的里。

4.遇到console.log(‘d’),直接印出。

5.本輪宏任務發現執行完畢,查看微任務,然後方法裡的函數,印出c。

6.本輪事件循環全部完成。

7.下引發循環,先執行宏任務,宏任務產生一個,印出一個setTimeout。

瘋狂的

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
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. 第一個宏任務(整體腳本)進入主線程,console.log(‘a’),印出a。

  2. 遇到setTimeout,其觸發功能進入宏任務,暫定義為setTimeout1。

  3. 遇到process.nextTick(),其原因函數被傳到微任務請求,暫定義為process1。

  4. 遇到Promise,new Promise直接執行,印出g。 then進入微任務,暫定義為then1。

  5. 遇到setTimeout,其觸發功能進入宏任務,暫定義為setTimeout2。

這時候我們看一下兩個任務中的情況

宏任務請求:setTimeout1、Timeout2

微任務請求:process1、then1

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

全部執行,印出f和h。

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

第二輪事件循環:

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

  2. 遇到process.nextTick(),進入微任務,暫定義為process2。

  3. Promise直接執行,然後進入微任務輸出,暫定義為then2。

這兩個任務中

宏任務請求:setTimeout2

微任務請求:process2、 then2

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

全部執行,印出和e。

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

第三輪事件循環

  1. 執行setTimeout2,遇到console.log(‘i’),印出i。

  2. 遇到process.nextTick(),進入微任務,暫定義為process3。

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

  4. then進入微任務,暫定義為then3。

這兩個任務中

宏任務請求:空

微任務請求:process3、then3

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

全部執行,印出j和l。

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

到此為止,三輪事件循環結束,最終輸出結果為:

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