淺析JavaScript的事件迴圈機制

polkYu發表於2019-04-09

本文為個人見解,如果發現文章有錯誤的地方,歡迎大家指正,感謝感謝~~

轉載請標明出處

本文針對於Chrome瀏覽器環境下的事件迴圈機制,node環境下還沒有進行試驗,以後再試驗下~~

前言

眾所周知,JavaScript的一大特點就是單執行緒,也就是會按順序執行程式碼,同一時間只能做一件事。

為什麼JavaScript會被設計成單執行緒?

JavaScript的誕生,一開始是為了解決瀏覽器使用者互動的問題,以及用來操作DOM,基於這個原因,JavaScript被設計成單執行緒,否則會帶來複雜的同步問題。

為什麼JavaScript需要非同步?

單執行緒意味著所有任務都要排隊進行,如果存在一個任務執行時間過長,後面的任務都會被阻塞,對於使用者而言就意味著“卡死”。

單執行緒的JavaScript是怎麼執行非同步程式碼的呢?

這就涉及到JavaScript的事件迴圈機制(event loop)了。

事件迴圈機制(event loop)

這裡先推薦去看看Philip Roberts的演講《Help, I’m stuck in an event-loop》,雖然內容沒有涉及到任務佇列的細分,但是對函式呼叫棧(call stack)的分析還是挺不錯的

列舉幾個概念:執行上下文函式呼叫棧(call stack), 任務佇列(task queue)

  • 執行上下文(以後有空應該會再寫一篇文章分析一下哈哈):
    • 全域性環境:JavaScript程式碼執行起來會首先進入該環境
    • 函式環境:當函式被呼叫執行時,會進入當前函式中執行程式碼
    • eval(不建議使用,可忽略)
  • 函式呼叫棧(call stack)是決定了js程式碼的執行機制,遇到函式時,會生成一個新的函式上下文,並且入棧,執行完畢後出棧
  • JavaScript中的任務分為macro-task(巨集任務)與micro-task(微任務)
  • macro-task包括:script(一段程式碼),setTimeout,setInterval,setImmediate,requestAnimationFrame,I/O,UI rendering
  • micro-task包括:process.nextTick, Promise, Object.observe, MutationObserver

淺析JavaScript的事件迴圈機制

  1. 當JavaScript程式碼開始執行時,首先將全域性環境壓入函式呼叫棧(棧底永遠都是全域性上下文,除非執行緒結束,在瀏覽器上表現為視窗關閉),之後,每遇到一個函式,建立一個新的函式上下文,並且入棧。

  2. 執行過程中,遇到了macro-task或者micro-task,都會將其交給對應的web api去處理,比如setTimeout交給timer模組,ajax請求交給network模組,DOM操作交給DOM對應模組處理,處理完成後,會將對應的回撥函式放入對應的佇列中(macro-task佇列以及micro-task佇列)

  3. 每當函式呼叫棧中的上下文都執行完畢時(全域性環境仍然存在),主程式會去查詢micro-task佇列,如果micro-task佇列為空,會取macro-task佇列第一個task放入呼叫棧執行,否則,取micro-task佇列的第一個task放入呼叫棧執行,如果在處理task期間,如果有新新增的microtasks或者macro-task,也會被新增到相應佇列的末尾

  4. 一直迴圈第3步,直至所有任務執行完畢,這就是事件迴圈

按照我的思路大概畫了個流程圖

淺析JavaScript的事件迴圈機制


來實踐一下,想象以下程式碼片段的控制檯輸出

console.log('start')

setTimeout(function setTimeout1() {
    console.log('setTimeout1')
    setTimeout(function setTimeout3() {
        console.log('setTimeout3')
        new Promise(function promise4(resolve, reject) {
            console.log('promise4')
            resolve('then')
        }).then(function then4() {
            console.log('promise4 then')
        })
    }, 0)

    new Promise(function promise3(resolve, reject) {
        console.log('promise3')
        setTimeout(function setTimeout4() {
            resolve('then')
        }, 0)
        console.log('after resolve')
    }).then(function then3() {
        console.log('promise3 then')
    })

}, 0)

new Promise(function promise1(resolve, reject) {
    console.log('promise1')
    resolve('then')
}).then(function then1() {
    console.log('promise1 then')
    new Promise(function promise2(resolve, reject) {
        console.log('promise2')
        resolve('then')
    }).then(function then2() {
        console.log('promise2 then')
    })
})

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



console.log('end')


/* 
控制檯輸出
start
promise1
end
promise1 then
promise2
promise2 then
setTimeout1
promise3
after resolve
setTimeout2
setTimeout3
promise3 then
*/

複製程式碼

細化步驟還挺多的,所以做了個gif~~

淺析JavaScript的事件迴圈機制

第一步

全域性上下文global進棧

全域性上下文global進棧

第二步

console.log('start')
複製程式碼

遇到console.log,函式進棧,呼叫web api的console介面,執行完成後出棧

淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制

第三步

setTimeout(function setTimeout1() {
 //....
}, 0)
複製程式碼

遇到setTimeout,交給timer模組執行,setTimeout出棧,timer執行完該定時器後(0秒後),將回撥函式setTimeout1放入macro-task隊尾。劃重點!!這是一個很容易產生誤解的地方,很多同學下意識都覺得定時器就是到了設定時間後立即執行,其實是到了時間後,將回撥函式放入macro-task佇列,等待執行

淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制

第四步

new Promise(function promise1(resolve, reject) {
    console.log('promise1')
    resolve('then')
}).then(function then1() {
    //...
})
複製程式碼

遇到promise,建構函式裡的promise1會立刻進棧並且執行,執行中遇到了resolve函式,進棧,將回撥函式then1放入micro-task佇列,此時promise1resolve都已執行完畢,出棧

淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制

第五步

setTimeout(function setTimeout2() {
    //...
}, 0)
複製程式碼

遇到setTimeout,交給timer模組執行,setTimeout出棧,timer執行完該定時器後(0秒後),將回撥函式setTimeout2放入macro-task隊尾。

淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制

第六步

console.log('end')
複製程式碼

淺析JavaScript的事件迴圈機制

第七步

到了很關鍵的一步,這個時候call stack已經執行完了(只剩下global),主程式會去查詢micro-task佇列,發現裡面有等待執行的函式,取隊首的函式(也就是then1)進棧執行

function then1() {
    console.log('promise1 then')
    new Promise(function promise2(resolve, reject) {
        console.log('promise2')
        resolve('then')
    }).then(function then2() {
        //...
    })
}
複製程式碼

在執行過程中,又遇到了promise,先執行建構函式裡的promise2,執行中遇到了resolve函式,進棧,將回撥函式then2放入micro-task佇列,此時then1promise2resolve都已執行完畢,出棧

淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制

第八步

call stack執行完畢,查詢micro-task佇列,發現裡面有等待執行的函式,取隊首的函式(也就是then2)進棧執行

function then2() {
    console.log('promise2 then')
}
複製程式碼

淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制

第九步

call stack執行完畢,查詢micro-task佇列,發現為空,查詢macro-task佇列,發現裡面有等待執行的函式,取隊首的函式(也就是setTimeout1)進棧執行

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

    new Promise(function promise3(resolve, reject) {
        console.log('promise3')
        setTimeout(function setTimeout4() {
            //...
        }, 0)
        console.log('after resolve')
    }).then(function then3() {
        //...
    })
}
複製程式碼

執行中遇到setTimeout,交給timer模組執行,setTimeout出棧,timer執行完該定時器後(0秒後),將回撥函式setTimeout3放入macro-task隊尾。 繼續執行,遇到了promise,先執行建構函式裡的promise3,又遇到了setTimeout,交給timer模組執行,setTimeout出棧,timer執行完該定時器後(0秒後),將回撥函式setTimeout4放入macro-task隊尾,此時setTimeout1promise3 都已執行完畢,出棧

淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制

第十步

call stack執行完畢,查詢micro-task佇列,發現為空,查詢macro-task佇列,發現裡面有等待執行的函式,取隊首的函式(也就是setTimeout2)進棧執行

function setTimeout2() {
    console.log('setTimeout2')
}
複製程式碼

淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制

第十步

call stack執行完畢,查詢micro-task佇列,發現為空,查詢macro-task佇列,發現裡面有等待執行的函式,取隊首的函式(也就是setTimeout3)進棧執行

function setTimeout3() {
    console.log('setTimeout3')
    new Promise(function promise4(resolve, reject) {
        console.log('promise4')
        resolve('then')
    }).then(function then4() {
        console.log('promise4 then')
    })
}
複製程式碼

執行中遇到了promise,先執行建構函式裡的promise4,遇到了resolve函式,進棧,將回撥函式then2放入micro-task佇列,此時setTimeout3promise4都已執行完畢,出棧

淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制

第十一步

call stack執行完畢,查詢micro-task佇列,發現裡面有等待執行的函式,取隊首的函式(也就是then4)進棧執行

function then4() {
    console.log('promise4 then')
}
複製程式碼

淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制

第十二步

call stack執行完畢,查詢micro-task佇列,發現為空,查詢macro-task佇列,發現裡面有等待執行的函式,取隊首的函式(也就是setTimeout4)進棧執行

function setTimeout4() {
    resolve('then')
}
複製程式碼

執行遇到resolve,將promise的回撥函式then3放入micro-task佇列,此時setTimeout4resolve已執行完畢,出棧

淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制

第十三步

call stack執行完畢,查詢micro-task佇列,發現裡面有等待執行的函式,取隊首的函式(也就是then3)進棧執行,執行完畢後出棧,至此全部程式碼執行完畢

淺析JavaScript的事件迴圈機制
淺析JavaScript的事件迴圈機制

呼~終於寫完了

總結

  1. 主程式開始執行程式碼時,先將全域性環境入棧,以後每遇到一個函式,建立一個新的上下文,進棧並且執行,遇到主程式執行不了的函式,交給web api執行,同時出棧
  2. 執行過程中,遇到了macro-task或者micro-task,都會將其交給對應的web api去處理,比如setTimeout交給timer模組,ajax請求交給network模組,DOM操作交給DOM對應模組處理,處理完成後,會將對應的回撥函式放入對應的佇列中(macro-task佇列以及micro-task佇列)
  3. 每當函式呼叫棧中的上下文都執行完畢時(全域性環境仍然存在),主程式會去查詢micro-task佇列,如果micro-task佇列為空,會取macro-task佇列第一個task放入呼叫棧執行,否則,取micro-task佇列的第一個task放入呼叫棧執行,如果在處理task期間,如果有新新增的microtasks或者macro-task,也會被新增到相應佇列的末尾
  4. 以上內容只針對於Chrome瀏覽器環境,node環境還沒有具體測試,好像是不太一樣的

關於

前端萌新一個~~打算經常寫寫文章總結一下知識點,歡迎關注,一起加油啦

相關文章