JavaScript事件迴圈(Event Loop)

heath_learning發表於2019-01-21

1、為什麼要有事件迴圈?

因為js是單執行緒的,事件迴圈是js的執行機制,也是js實現非同步的一種方法。

既然js是單執行緒,那就像只有一個視窗的銀行,客戶需要排隊一個一個辦理業務,同理js任務也要一個一個順序執行。如果一個任務耗時
過長,那麼後一個任務也必須等著。那麼問題來了,假如我們想瀏覽新聞,但是新聞包含的超清圖片載入很慢,難道我們的網頁要一直卡著
直到圖片完全顯示出來?因此聰明的程式設計師將任務分為兩類:

  • 同步任務
  • 非同步任務

當我們開啟網站時,網頁的渲染過程就是一大堆同步任務,比如頁面骨架和頁面元素的渲染。而像載入圖片音樂之類佔用資源大耗時久的任務,
就是非同步任務。

2、巨集任務與微任務

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

  • macro-task(巨集任務): 包括整體程式碼scriptsetTimeoutsetInterval
  • micro-task(微任務): Promiseprocess.nextTick

不同的型別的任務會進入不同的Event Queue(事件佇列),比如setTimeout、setInterval會進入一個事件佇列,而Promise會進入
另一個事件佇列。

一次事件迴圈中有巨集任務佇列和微任務佇列。事件迴圈的順序,決定js程式碼執行的順序。進入整體程式碼(巨集任務-<script>包裹的程式碼可以
理解為第一個巨集任務),開始第一次迴圈,接著執行所有的微任務。然後再次從巨集任務開始,找到其中一個任務佇列的任務執行完畢,
再執行所有的微任務。如:
<script>
    setTimeout(function() {
        console.log(`setTimeout`);
    })

    new Promise(function(resolve) {
        console.log(`promise`);
    }).then(function() {
        console.log(`then`);
    })

    console.log(`console`);

    /* ----------------------------分析 start--------------------------------- */

    1、`<script>`中的整段程式碼作為第一個巨集任務,進入主執行緒。即開啟第一次事件迴圈
    2、遇到setTimeout,將其回撥函式放入Event table中註冊,然後分發到巨集任務Event Queue中
    3、接下來遇到new Promise、Promise,立即執行;將then函式分發到微任務Event Queue中。輸出: promise
    4、遇到console.log,立即執行。輸出: console
    5、整體程式碼作為第一個巨集任務執行結束,此時去微任務佇列中檢視有哪些微任務,結果發現了then函式,然後將它推入主執行緒並執行。
輸出: then
    6、第一輪事件迴圈結束
    開啟第二輪事件迴圈。先從巨集任務開始,去巨集任務事件佇列中檢視有哪些巨集任務,在巨集任務事件佇列中找到了setTimeout對應的回撥函式,
立即執行之。此時巨集任務事件佇列中已經沒有事件了,然後去微任務事件佇列中檢視是否有事件,結果沒有。此時第二輪事件迴圈結束;
輸出:setTimeout

    /* ----------------------------分析 end--------------------------------- */
</script>

3、分析更復雜的程式碼

<script>
    console.log(`1`);

    setTimeout(function() {
        console.log(`2`);
        process.nextTick(function() {
            console.log(`3`);
        })
        new Promise(function(resolve) {
            console.log(`4`);
            resolve();
        }).then(function() {
            console.log(`5`)
        })
    })
    process.nextTick(function() {
        console.log(`6`);
    })
    new Promise(function(resolve) {
        console.log(`7`);
        resolve();
    }).then(function() {
        console.log(`8`)
    })

    setTimeout(function() {
        console.log(`9`);
        process.nextTick(function() {
            console.log(`10`);
        })
        new Promise(function(resolve) {
            console.log(`11`);
            resolve();
        }).then(function() {
            console.log(`12`)
        })
    })
</script>

一、第一輪事件迴圈

a)、整段<script>程式碼作為第一個巨集任務進入主執行緒,即開啟第一輪事件迴圈
b)、遇到console.log,立即執行。輸出:1
c)、遇到setTimeout,將其回撥函式放入Event table中註冊,然後分發到巨集任務事件佇列中。我們將其標記為setTimeout1
d)、遇到process.nextTick,其回撥函式放入Event table中註冊,然後被分發到微任務事件佇列中。記為process1
e)、遇到new Promise、Promise,立即執行;then回撥函式放入Event table中註冊,然後被分發到微任務事件佇列中。記為then1。
輸出: 7
f)、遇到setTimeout,將其回撥函式放入Event table中註冊,然後分發到巨集任務事件佇列中。我們將其標記為setTimeout2

此時第一輪事件迴圈巨集任務結束,下表是第一輪事件迴圈巨集任務結束時各Event Queue的情況

巨集任務事件佇列 微任務事件佇列
第一輪事件迴圈 (巨集任務已結束) process1、then1
第二輪事件迴圈(未開始) setTimeout1
第三輪事件迴圈(未開始) setTimeout2

可以看到第一輪事件迴圈巨集任務結束後微任務事件佇列中還有兩個事件待執行,因此這兩個事件會被推入主執行緒,然後執行

g)、執行process1。輸出:6
h)、執行then1。輸出:8

第一輪事件迴圈正式結束!


二、第二輪事件迴圈

a)、第二輪事件迴圈從巨集任務setTimeout1開始。遇到console.log,立即執行。輸出: 2
b)、遇到process.nextTick,其回撥函式放入Event table中註冊,然後被分發到微任務事件佇列中。記為process2
c)、遇到new Promise,立即執行;then回撥函式放入Event table中註冊,然後被分發到微任務事件佇列中。記為then2。輸出: 5

此時第二輪事件迴圈巨集任務結束,下表是第二輪事件迴圈巨集任務結束時各Event Queue的情況

巨集任務事件佇列 微任務事件佇列
第一輪事件迴圈(已結束)
第二輪事件迴圈 (巨集任務已結束) process2、then2
第三輪事件迴圈(未開始) setTimeout2

可以看到第二輪事件迴圈巨集任務結束後微任務事件佇列中還有兩個事件待執行,因此這兩個事件會被推入主執行緒,然後執行

d)、執行process2。輸出:3
e)、執行then2。輸出:5

第二輪事件迴圈正式結束!


三、第三輪事件迴圈

a)、第三輪事件迴圈從巨集任務setTimeout2開始。遇到console.log,立即執行。輸出: 9
d)、遇到process.nextTick,其回撥函式放入Event table中註冊,然後被分發到微任務事件佇列中。記為process3
c)、遇到new Promise,立即執行;then回撥函式放入Event table中註冊,然後被分發到微任務事件佇列中。記為then3。輸出: 11

此時第三輪事件迴圈巨集任務結束,下表是第三輪事件迴圈巨集任務結束時各Event Queue的情況

巨集任務事件佇列 微任務事件佇列
第一輪事件迴圈(已結束)
第二輪事件迴圈(已結束)
第三輪事件迴圈(未開始) (巨集任務已結束) process3、then3

可以看到第二輪事件迴圈巨集任務結束後微任務事件佇列中還有兩個事件待執行,因此這兩個事件會被推入主執行緒,然後執行

d)、執行process3。輸出:10
e)、執行then3。輸出:12

4、js事件迴圈總結

1)、js執行從最先進入任務佇列的巨集任務開始,通常是整體<scirpt>程式碼
2)、巨集任務佇列事件全部執行完畢後,檢查微任務佇列是否有事件,有則執行,直到沒有事件為止
3)、更新render(每一次事件迴圈,瀏覽器都可能會去更新渲染)
4)、重複以上步驟

5、參考文章

https://juejin.im/post/59e85e…
https://segmentfault.com/a/11…

相關文章