筆試題之Event Loop終極篇

先上一道常見的筆試題

console.log('1');
async function async1() {
    console.log('2');
    await async2();
    console.log('3');
}
async function async2() {
    console.log('4');
}

process.nextTick(function() {
    console.log('5');
})

setTimeout(function() {
    console.log('6');
    process.nextTick(function() {
        console.log('7');
    })
    new Promise(function(resolve) {
        console.log('8');
        resolve();
    }).then(function() {
        console.log('9')
    })
})

async1();

new Promise(function(resolve) {
    console.log('10');
    resolve();
}).then(function() {
    console.log('11');
});
console.log('12');

大家可以先配合下面這個圖片思考一下輸出順序及這麼運行的原因
圖片描述

上面簡化圖解可拆分爲三部分:

一、JavaScript引擎

*Memory Heap 內存堆 —— 這是內存發生分配的地方

*Call Stack 調用棧 —— 這是代碼運行時棧幀存放的位置

二、Runtime 運行時

我們要知道的是,像setTimeOut DOM AJAX,都不是由JavaScript引擎提供,而是由瀏覽器提供,統稱爲Web APIs

三、EventLoop

1、關於javascript

javascript是一門單線程語言,雖然HTML5提出了Web-works這樣的多線程解決方案,但是並沒有改變JaveScript是單線程的本質。

什麼是H5 Web Works?

就是將一些大計算量的代碼交由web Worker運行而不凍結用戶界面,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單線程的本質

2、javascript事件循環

既然js是單線程的,就是同一時間只能做一件事情。那麼問題來了,我們訪問一個頁面,這個頁面的初始化代碼運行時間很長,比如有很多圖片、視頻、外部資源等等,難道我們也要一直在那等着嗎?答案當然是 不能

所以就出現了兩類任務:

  • 同步任務
  • 異步任務

圖片描述

  1. 同步和異步任務分別進入不同的 '‘場所'’ 執行。所有同步任務都在主線程上執行,形成一個執行棧;而異步任務進入Event Table並註冊回調函數
  2. 當這個異步任務有了運行結果,Event Table會將這個回調函數移入Event Queue,進入等待狀態
  3. 當主線程內同步任務執行完成,會去Event Queue讀取對應的函數,並結束它的等待狀態,進入主線程執行
  4. 主線程不斷重複上面3個步驟,也就是常說的Event Loop(事件循環)。
那麼我們怎麼知道什麼時候主線程是空的呢?

js引擎存在monitoring process進程,會持續不斷的檢查主線程執行棧是否爲空,一旦爲空,就會去Event Queue那裏檢查是否有等待被調用的函數。

3、setTimeout和setInterval

setTimeout(fn,0)這裏的延遲0秒時什麼意思呢?

含義是,當主線程執行棧內爲空時,不用等待,就馬上執行。

setInterval和setTimeout類似,只是前者是循環的執行。對於執行順序來說,setInterval會每隔指定的時間將註冊的函數置入Event Queue,如果前面的任務耗時太久,那麼同樣需要等待。

對於setInterval(fn,ms)來說,我們已經知道不是每過ms秒會執行一次fn,而是每過ms秒,會有fn進入Event Queue。一旦setInterval的回調函數fn執行時間超過了延遲時間ms,那麼就完全看不出來有時間間隔了

4、promise和process.nextTick與async/await

事件循環的順序,決定js代碼的執行順序。進入整體代碼(宏任務)後,開始第一次循環。接着執行所有的微任務。然後再次從宏任務開始,找到其中一個任務隊列執行完畢,再執行所有的微任務。

除了廣義的同步任務和異步任務,我們對任務有更精細的定義:

  • macro-task(宏任務):包括整體代碼script、setTimeout、setInterval、I/O、UI交互事件,可以理解是每次執行棧執行的代碼就是一個宏任務;
  • micro-task(微任務):Promise,process.nextTick,且process.nextTick優先級大於promise.then。可以理解是在當前 task 執行結束後立即執行的任務;

setTimeout(fn, 0)在下一輪“事件循環”開始時執行,Promise.then()在本輪“事件循環”結束時執行。

不同類型的任務會進入對應的Event Queue:

Promise中的異步體現在thencatch中,所以寫在Promise中的代碼是被當做同步任務立即執行的。

await實際上是一個讓出線程的標誌。await後面的表達式會先執行一遍,將await後面的代碼加入到microtask中,然後就會跳出整個async函數來執行後面的代碼;

因爲async await 本身就是promise+generator的語法糖。所以await後面的代碼是microtask。

下面開始分析開頭的代碼

console.log('1');
async function async1() {
    console.log('2');
    await async2();
    console.log('3');
}
async function async2() {
    console.log('4');
}

process.nextTick(function() {
    console.log('5');
})

setTimeout(function() {
    console.log('6');
    process.nextTick(function() {
        console.log('7');
    })
    new Promise(function(resolve) {
        console.log('8');
        resolve();
    }).then(function() {
        console.log('9')
    })
})

async1();

new Promise(function(resolve) {
    console.log('10');
    resolve();
}).then(function() {
    console.log('11');
});
console.log('12');

第一輪事件循環流程:

  • 整體script作爲第一個宏任務進入主線程,遇到console.log,輸出1
  • 遇到async1、async2函數聲明,聲明暫時不用管
  • 遇到process.nextTick(),其回調函數被分發到微任務Event Queue中。我們記爲process1
  • 遇到setTimeout,其回調函數被分發到宏任務Event Queue中。我們暫且記爲setTimeout1
  • 執行async1,遇到console.log,輸出2

下面這裏是最難理解的地方

我們知道使用 async 定義的函數,當它被調用時,它返回的是一個Promise對象

而當await後面的表達式是一個Promise時,它的返回值實際上是Promise的回調函數resolve的參數

  • 遇到await async2()調用,發現async2也是一個 async 定義的函數,所有直接執行輸出4,同時返回了一個Promise。劃重點:此時返回的Promise被分配到微任務Event Queue中,我們記爲await1。await會讓出線程,接下來就會跳出async1函數繼續往下執行。
  • 遇到Promisenew Promise直接執行,輸出10。then被分發到微任務Event Queue中。我們記爲then1
  • 遇到console.log,輸出12
宏任務Event Queue 微任務Event Queue
setTimeout1 process1
await1
then1

上表是第一輪事件循環宏任務結束時各Event Queue的情況,此時已經輸出了1 2 4 10 12

我們發現了process1await1 then1三個微任務

  • 執行process1,輸出5
  • 取到 await1 ,就是 async1 放進去的Promise,執行Promise時發現又遇到了他的真命天子resolve函數,劃重點:這個resolve又會被放入微任務Event Queue中,我們記爲await2,然後再次跳出 async1函數 繼續下一個任務。
  • 執行then1,輸出11
宏任務Event Queue 微任務Event Queue
setTimeout1 await2

到這裏,已經輸出了1 2 4 10 12 5 11

此時還有一個await2 微任務

它是async1 放進去的Promise的resolve回調,執行它(因爲 async2 並沒有return東西,所以這個resolve的參數是undefined),此時 await 定義的這個 Promise 已經執行完並且返回了結果,所以可以繼續往下執行 async1函數 後面的任務了,那就是console.log(3),輸出3

到這裏,第一輪事件循環結束,此時,輸出順序是 1 2 4 10 12 5 11 3

第二輪時間循環從setTimeout1宏任務開始

  • 遇到console.log,輸出6
  • 遇到process.nextTick(),同樣將其分發到微任務Event Queue中,記爲process2
  • 遇到new Promise立即執行輸出8,then也分發到微任務Event Queue中,記爲then2
宏任務Event Queue 微任務Event Queue
process2
then2

上表是第二輪事件循環宏任務結束時各Event Queue的情況,此時輸出情況是

我們發現了process2then2兩個微任務

  • 執行process2,輸出7
  • 執行then2,輸出9

第二輪事件循環結束,第二輪輸出6 8 7 9

整段代碼,共進行了兩次事件循環,完整的輸出 1 2 4 10 12 5 11 3 6 8 7 9

四、寫在最後

到這裏,大家應該已經清楚了JS的事件循環機制,後面不管在工作還是面試中,肯定都是遊刃有餘啦~

本篇是我開始的第一篇文章,還希望大家多多支持,不吝賜教哇,也希望可以提出意見或建議。

資料參考:

promise、async和await之執行順序的那點事
這一次,徹底弄懂 JavaScript 執行機制

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