瀏覽器的event loop和node的event loop

金大光發表於2018-05-27

1.什麼是event loop

event loops也就是事件迴圈,它是為了協調事件(event),使用者互動(user interaction),指令碼(script),渲染(rendering),網路(networking),使用者代理(user agent)的工作而產生的一個機制。

2.JavaScript的執行機制

2.1 單執行緒的JavaScript

JavaScript語言的一大特點就是單執行緒,也就是說在同一時間只做同一件事。這是基於js的執行環境決定的,因為在瀏覽器中,有許多的dom操作,如果在同一時間操作一個dom,很容易造成混亂,所以為了避免發生同一時間操作同一dom的情況,js選擇只用一個主執行緒執行程式碼,來保證程式執行的一致性,單執行緒的特點也應用到了node中。

2.2 JavaScript中的任務和佇列

JavaScript是單執行緒的,也就意味著所有任務需要排隊,前一個任務執行完,才能執行下一個任務,但是因為IO裝置(輸入輸出裝置)很慢(比如Ajax從網路讀取資料),不得不等待結果返回之後才能繼續,這樣的執行效率很慢。 於是分成了兩種任務來處理,同步任務和非同步任務。 同步任務是指在主執行緒排隊的任務,只有前面的任務執行完之後才執行後面的任務。 非同步任務指的是任務不進入主執行緒,而進入到一個任務佇列(task queue),主執行緒的任務可以繼續往後執行,而在任務佇列裡的非同步任務執行完會通知主執行緒。

3.瀏覽器的event loop

3.1執行棧與事件佇列

當javascript程式碼執行的時候會將不同的變數存於記憶體中的不同位置:堆(heap)和棧(stack)中來加以區分。其中,堆裡存放著一些物件。而棧中則存放著一些基礎型別變數以及物件的指標。當所有所有同步任務都在主執行緒上執行時,這些任務被排列在一個單獨的地方,形成一個執行棧

當瀏覽器js引擎解析這段程式碼時,會將同步任務順序加入執行棧中依次執行,當遇到非同步任務時並不會一直等待非同步任務返回結果再執行後面的任務,而是將非同步任務掛起,繼續執行同步任務,當非同步任務返回結果時,將非同步任務的回撥事件加入到一個事件佇列(Task Queue)當中去,這個事件佇列裡的任務並不會立即執行,而是等同步任務全部執行完,再依次執行事件佇列裡的事件。

依次執行同步任務,完成後依次執行事件佇列,完成後再去執行同步任務,這樣形成了一個迴圈,就是事件迴圈(Event Loop)。

瀏覽器的event loop和node的event loop

3.2巨集任務(macro task)與微任務(micro task)

非同步任務又分為巨集任務與微任務兩種,微任務並不是老老實實的按照事件佇列的順序去執行,而是按照microTask—>macroTask的順序去執行,先執行完佇列中所有的microTask再去執行macroTask

巨集任務和微任務的分類

  • MacroTask: script(整體程式碼), setTimeout, setInterval, setImmediate(node獨有), I/O, UI rendering

  • MicroTask: process.nextTick(node獨有), Promises, Object.observe(廢棄), MutationObserver

舉個例子
setTimeout(()=>{
    console.log(1)
})

Promise.resolve().then(function() {
    console.log(2)
})
console.log(3)

執行結果是:3 2 1
這是因為事件迴圈的順序是:同步程式碼=>微任務=>巨集任務
複製程式碼

4.node的event loop

  • timers: 這個階段執行定時器佇列中的回撥如 setTimeout() 和 setInterval()。

  • I/O callbacks: 這個階段執行幾乎所有的回撥。但是不包括close事件,定時器和setImmediate()的回撥。

  • idle, prepare: 這個階段僅在內部使用,可以不必理會。

  • poll: 等待新的I/O事件,node在一些特殊情況下會阻塞在這裡。

  • check: setImmediate()的回撥會在這個階段執行。

  • close callbacks: 例如socket.on('close', ...)這種close事件的回撥。

瀏覽器的event loop和node的event loop
event loop的每一次迴圈都需要依次經過上述的階段。 每個階段都有自己的callback佇列,每當進入某個階段,都會從所屬的佇列中取出callback來執行,當佇列為空或者被執行callback的數量達到系統的最大數量時,進入下一階段。這六個階段都執行完畢稱為一輪迴圈。

舉個例子(1)
瀏覽器與Node執行順序的區別
setTimeout(()=>{
    console.log('timer1')

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

setTimeout(()=>{
    console.log('timer2')

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

複製程式碼

瀏覽器輸出: time1 promise1 time2 promise2 因為promise是microtask,所以當第一個setTimeout執行完之後,先執行promise。

Node輸出: time1 time2 promise1 promise2 因為time1和time2都在timers階段,所以先執行timers,promise的回撥被加入到了microtask佇列,等到timers階段執行完畢,在去執行microtask佇列。

舉個例子(2)
MicroTask佇列與MacroTask佇列
setTimeout(function () {
   console.log(1);
});
console.log(2);
process.nextTick(() => {
   console.log(3);
});
new Promise(function (resolve, rejected) {
   console.log(4);
   resolve()
}).then(res=>{
   console.log(5);
})
setImmediate(function () {
   console.log(6)
})
console.log('end');
複製程式碼

node輸出的順序是 2 4 end 3 5 1 6 首先執行的是同步任務中的2 4 end,然後是microTask佇列中的process.nextTick:3、promise.then:5,最後是macroTask佇列中的setTimeout:1、setImmediate:6,由於Timer優於Check階段,所以先1後6。

相關文章