從幾道題目帶你深入理解Event Loop_宏隊列_微隊列


深入探究JavaScript的Event Loop

Javascript是一門單線程語言

但是在運行時難免會遇到需要較長執行時間的任務如: 向后端服務器發送請求。 其他的任務不可能都等它執行完才執行的(同步)否則效率太低了, 於是異步的概念就此產生: 當遇到需要較長時間的任務時將其放入"某個地方"后繼續執行其他同步任務, 等所有同步任務執行完畢后再poll(輪詢)剛剛這些需要較長時間的任務並得到其結果

而處理異步任務的這一套流程就叫Event Loop即事件循環,是瀏覽器或Node的一種解決javaScript單線程運行時不會阻塞的一種機制, 於是更完善的說法是: Javascript是一門單線程非阻塞語言

Event Loop的結構

  • 堆(heap): 用於存放JS對象的數據結構
  • 調用棧(stack): 同步任務會按順序在調用棧中等待主線程依次執行
  • Web API: 是瀏覽器/Node 用於處理異步任務的地方
  • 回調隊列(callbacks queue): 經過Web API處理好的異步任務會被一次放入回調隊列中, 等一定條件成立后被逐個poll(輪詢)放入stack中被主線程執行

回調隊列(callbacks queue)的分類

回調隊列(callbacks queue)進而可以細分為

  1. 宏任務(macroTasks)

    • script全部代碼、
    • setTimeout、
    • setInterval、
    • setImmediate(瀏覽器暫時不支持,只有IE10支持,具體可見MDN)、
    • I/O、UI Rendering
  2. 微任務(microTasks)

    • Process.nextTick(Node獨有)
    • MutationObserver
    • Promise、
    • Object.observe(廢棄)

Event Loop的執行順序

  1. 首先順序執行初始化代碼(run script), 同步代碼放入調用棧中執行, 異步代碼放入對應的隊列中
  2. 所有同步代碼執行完畢后,確認調用棧(stack)是否為空, 只有stack為為空才能開始按照隊列的特性輪詢執行 微任務隊列中的代碼
  3. 只有當所有微任務隊列中的任務執行完后, 才能執行宏任務隊列中的下一個任務

用流程圖表示:

通過題目來深入

題目1:

setTimeout(() => {
    console.log(1)
}, 0)
Promise.resolve().then(
    () => {
        console.log(2)
    }
)
Promise.resolve().then(
    () => {
        console.log(4)
    }
)
console.log(3)
  1. 執行初始化代碼

  2. 初始化代碼執行完畢, script 任務結束, 調用棧為空 所以可以開始輪詢執行微任務隊列的代碼

    1. 取出第一個微任務到調用棧中執行--打印2, 執行完后調用棧為空, 檢查微任務隊列是否還有任務有則執行

    2. 取出第二個微任務到調用棧中執行--打印4, 執行完后調用棧為空, 微任務隊列為空

  3. 第一個宏任務(run script)完成, 可以輪詢宏任務隊列的下一個任務

  4. 開始輪詢執行宏任務隊列中的下一個任務

  5. 最終整個執行順序、結果如圖所示:

於是這道題最終的結果是:

3 2 4 1

題目2:

setTimeout(()=>{
    console.log(1)
}, 0)


new Promise((resolve, reject) => {
    console.log(2)
    resolve()
})
.then(
    () => {
        console.log(3)
    }
)
.then(
    () => {
        console.log(4)
    }
)
console.log(5)
  1. 執行初始化代碼

  2. 初始化代碼執行完畢, script 任務結束, 調用棧為空所以可以開始輪詢執行微任務隊列的代碼

    1. 取出第一個微任務到調用棧中執行--打印3, 執行完后調用棧為空, 此時第一個then()返回的Promise有了狀態、結果, 於是將第二個then()放入微任務隊列中, 檢查微任務隊列是否還有任務有則執行

    1. 調用棧、微任務隊列為空, run script執行完畢
  3. 開始輪詢執行宏任務隊列中的下一個任務

  4. 最終整個執行順序、結果如圖所示:

於是這道題最終的結果是:

2 5 3 4 1

題目3:

const first = () => {
    return new Promise((resolve, reject) => {
        console.log(3)
        let p = new Promise((resolve, reject) => {
            console.log(7)
            setTimeout(() => {
                console.log(5)
            }, 0)
            resolve(1)
        })
        resolve(2)
        p.then(
            arg => {
                console.log(arg)
            }
        )
    })
}

first().then(
    arg => {
        console.log(arg)
    }
)

console.log(4)
  1. 執行初始化代碼

  2. 初始化代碼執行完畢, script 任務結束, 調用棧為空所以可以開始輪詢執行微任務隊列的代碼

    1. 取出第一個微任務到調用棧中執行--打印1, 執行完后調用棧為空, 檢查微任務隊列是否還有任務有則執行

    1. 調用棧、微任務隊列為空, run script執行完畢
  3. 開始輪詢執行宏任務隊列中的下一個任務

  4. 最終整個執行順序、結果如圖所示:

於是這道題最終的結果是:

3 7 4 1 2 5

題目4:

setTimeout(()=>{
    console.log(0)
}, 0)


new Promise((resolve, reject) => {
    console.log(1)
    resolve()
})
.then(
    () => {
        console.log(2)
        new Promise((resolve, reject) => {
            console.log(3)
            resolve()
        })
        .then(
            () => console.log(4)
        )
        .then(
            () => console.log(5)
        )
    }
)
.then(
    () => console.log(6)
)

new Promise((resolve, reject) => {
    console.log(7)
    resolve()
})
.then(
    () => console.log(8)
)
  1. 執行初始化代碼

  2. 初始化代碼執行完畢, script 任務結束=, 所以可以開始輪詢執行微任務隊列的代碼

    1. 取出第一個任務到調用棧--執行onResolved中的所有代碼

    2. 很重要的地方是此時第一個new Promise的第二個then此時會被放入微任務隊列中

    3. 執行完后調用棧為空, 檢查微任務隊列是否還有任務有則執行

    1. 調用棧、微任務隊列為空, run script執行完畢
  3. 開始輪詢執行宏任務隊列中的下一個任務

  4. 最終整個執行順序、結果如圖所示:

於是這道題最終的結果是:

1 7 2 3 8 4 6 5 0

題目5:

console.log('script start')

async function async1() {
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2 end')
}
async1()

setTimeout(function () {
    console.log('setTimeout')
}, 0)

new Promise(resolve => {
    console.log('Promise')
    resolve()
})
.then(function () {
    console.log('promise1')
})
.then(function () {
    console.log('promise2')
})

console.log('script end')
  1. 執行初始化代碼

  2. 初始化代碼執行完畢, script 任務結束=, 所以可以開始輪詢執行微任務隊列的代碼

    1. 取出第一個任務到調用棧--執行await后的所有代碼, 執行完后調用棧為空, 檢查微任務隊列是否還有任務有則執行

    1. 調用棧、微任務隊列為空, 宏任務run script執行完畢
  3. 開始輪詢執行宏任務隊列中的下一個任務

  4. 最終整個執行順序、結果如圖所示:

於是這道題最終的結果是:

script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout

終極題1:

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <style>
        .outer {
            width: 200px;
            height: 200px;
            background-color: orange;
        }

        .inner {
            width: 100px;
            height: 100px;
            background-color: salmon;
        }
    </style>
</head>

<body>
    <div class="outer">
        <div class="inner"></div>
    </div>

    <script>
        var outer = document.querySelector('.outer')
        var inner = document.querySelector('.inner')

        new MutationObserver(function () {
            console.log('mutate')
        }).observe(outer, {
            attributes: true,
        })

        function onClick() {
            console.log('click')

            setTimeout(function () {
                console.log('timeout')
            }, 0)

            Promise.resolve().then(function () {
                console.log('promise')
            })

            outer.setAttribute('data-random', Math.random())
        }

        inner.addEventListener('click', onClick)
        outer.addEventListener('click', onClick)
    </script>
</body>
</html>
  1. 執行初始化代碼

  2. 初始化代碼執行完畢, 調用棧為空所以可以開始輪詢執行微任務隊列的代碼

    1. 取出第一個任務到調用棧--打印promise, 執行完后調用棧為空, 檢查微任務隊列是否還有任務有則執行

    1. 調用棧、微任務隊列為空, 因為存在冒泡, 所以以上操作再進行一次
  3. 宏任務run script執行完畢, 調用棧、微任務隊列為空可以輪詢執行宏任務隊列中的下一個任務

  4. 開始輪詢執行宏任務隊列中的下一個任務

  5. 微任務隊列、調用棧為空, 繼續輪詢執行宏任務隊列中的下一個任務

於是這道題最終的結果是:

click
promise
mutate
click
promise
mutate
timeout
timeout

不同瀏覽器下的不同結果(如果你的結果在這其中, 也是對的)

這里令人迷惑的點是: outer的冒泡執行為什么比outer的setTimeout先

那是因為:

  • 首先outer的setTimeout是一個宏任務, 它進入宏任務隊列時是在了run script的后面
  • inner執行到mutate后run script並沒有執行完, 而是還有一個outer.click的冒泡要執行
  • 只有執行完該冒泡后, run script才真正執行完(才可以執行下一個宏任務)

終極題2:

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <style>
        .outer {
            width: 200px;
            height: 200px;
            background-color: orange;
        }

        .inner {
            width: 100px;
            height: 100px;
            background-color: salmon;
        }
    </style>
</head>

<body>
    <div class="outer">
        <div class="inner"></div>
    </div>

    <script>
        var outer = document.querySelector('.outer')
        var inner = document.querySelector('.inner')

        new MutationObserver(function () {
            console.log('mutate')
        }).observe(outer, {
            attributes: true,
        })

        function onClick() {
            console.log('click')

            setTimeout(function () {
                console.log('timeout')
            }, 0)

            Promise.resolve().then(function () {
                console.log('promise')
            })

            outer.setAttribute('data-random', Math.random())
        }

        inner.addEventListener('click', onClick)
        outer.addEventListener('click', onClick)
        inner.click()   // 模擬點擊inner

    </script>
</body>
</html>
  1. 執行初始化代碼, 這里與終極題1不同的地方在於: 終極題1的click是作為回調函數(dispatch), 而這里是直接同步調用的

  2. inner.click執行完畢, inner.click退棧, 由於調用棧並不為空, 所以不能輪詢微任務隊列, 而是繼續執行run script(執行冒泡部分)
    需要注意: 由於outer.click的MutationObserver並未執行所以不會被再次添加進微任務隊列中

  3. inner.click退棧, 宏任務run script執行完畢, run script也退棧 調用棧為空, 開始輪詢微任務隊列

  4. 調用棧、微任務隊列為空, 開始輪詢執行宏任務隊列中的下一個任務

  5. 微任務隊列、調用棧為空, 繼續輪詢執行宏任務隊列中的下一個任務

於是這道題最終的結果是:

click
click
promise
mutate
promise
timeout
timeout

參考文章:

一次弄懂Event Loop(徹底解決此類面試問題)

Tasks, microtasks, queues and schedules


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM