帶你完全弄懂Event Loop

前言

我在學習瀏覽器和NodeJS的Event Loop時看了大量的文章,那些文章都寫的很好,可是每每是每篇文章有那麼幾個關鍵的點,不少篇文章湊在一塊兒綜合來看,才能夠對這些概念有較爲深刻的理解。html

因而,我在看了大量文章以後,想要寫這麼一篇博客,不採用官方的描述,結合本身的理解以及示例代碼,用最通俗的語言表達出來。但願你們能夠經過這篇文章,瞭解到Event Loop究竟是一種什麼機制,瀏覽器和NodeJS的Event Loop又有什麼區別。若是在文中出現書寫錯誤的地方,歡迎你們留言一塊兒探討。html5

(PS:說到Event Loop確定會提到Promise,我根據Promise A+規範本身實現了一個簡易Promise庫,源碼放到Github上,你們有須要的能夠當作參考,後續我也會也寫一篇博客來說Promise,若是對你有用,就請給個Star吧~)node

正文

Event Loop是什麼

event loop是一個執行模型,在不一樣的地方有不一樣的實現。瀏覽器和NodeJS基於不一樣的技術實現了各自的Event Loop。git

  • 瀏覽器的Event Loop是在html5的規範中明肯定義。
  • NodeJS的Event Loop是基於libuv實現的。能夠參考Node的官方文檔以及libuv的官方文檔
  • libuv已經對Event Loop作出了實現,而HTML5規範中只是定義了瀏覽器中Event Loop的模型,具體的實現留給了瀏覽器廠商。

宏隊列和微隊列

宏隊列,macrotask,也叫tasks。 一些異步任務的回調會依次進入macro task queue,等待後續被調用,這些異步任務包括:github

  • setTimeout
  • setInterval
  • setImmediate (Node獨有)
  • requestAnimationFrame (瀏覽器獨有)
  • I/O
  • UI rendering (瀏覽器獨有)

微隊列,microtask,也叫jobs。 另外一些異步任務的回調會依次進入micro task queue,等待後續被調用,這些異步任務包括:web

  • process.nextTick (Node獨有)
  • Promise
  • Object.observe
  • MutationObserver

(注:這裏只針對瀏覽器和NodeJS)segmentfault

瀏覽器的Event Loop

咱們先來看一張圖,再看完這篇文章後,請返回來再仔細看一下這張圖,相信你會有更深的理解。api

browser-eventloop

這張圖將瀏覽器的Event Loop完整的描述了出來,我來說執行一個JavaScript代碼的具體流程:promise

  1. 執行全局Script同步代碼,這些同步代碼有一些是同步語句,有一些是異步語句(好比setTimeout等);
  2. 全局Script代碼執行完畢後,調用棧Stack會清空;
  3. 從微隊列microtask queue中取出位於隊首的回調任務,放入調用棧Stack中執行,執行完後microtask queue長度減1;
  4. 繼續取出位於隊首的任務,放入調用棧Stack中執行,以此類推,直到直到把microtask queue中的全部任務都執行完畢。注意,若是在執行microtask的過程當中,又產生了microtask,那麼會加入到隊列的末尾,也會在這個週期被調用執行
  5. microtask queue中的全部任務都執行完畢,此時microtask queue爲空隊列,調用棧Stack也爲空;
  6. 取出宏隊列macrotask queue中位於隊首的任務,放入Stack中執行;
  7. 執行完畢後,調用棧Stack爲空;
  8. 重複第3-7個步驟;
  9. 重複第3-7個步驟;
  10. ......

能夠看到,這就是瀏覽器的事件循環Event Loop瀏覽器

這裏概括3個重點:

  1. 宏隊列macrotask一次只從隊列中取一個任務執行,執行完後就去執行微任務隊列中的任務;
  2. 微任務隊列中全部的任務都會被依次取出來執行,知道microtask queue爲空;
  3. 圖中沒有畫UI rendering的節點,由於這個是由瀏覽器自行判斷決定的,可是隻要執行UI rendering,它的節點是在執行完全部的microtask以後,下一個macrotask以前,緊跟着執行UI render。

好了,概念性的東西就這麼多,來看幾個示例代碼,測試一下你是否掌握了:

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  console.log(data);
})

setTimeout(() => {
  console.log(6);
})

console.log(7);
複製代碼

這裏結果會是什麼呢?運用上面瞭解到的知識,先本身作一下試試看。

// 正確答案
1
4
7
5
2
3
6
複製代碼

你答對了嗎?

咱們來分析一下整個流程:


  1. 執行全局Script代碼

Step 1

console.log(1)
複製代碼

Stack Queue: [console]

Macrotask Queue: []

Microtask Queue: []

打印結果:
1

Step 2

setTimeout(() => {
  // 這個回調函數叫作callback1,setTimeout屬於macrotask,因此放到macrotask queue中
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});
複製代碼

Stack Queue: [setTimeout]

Macrotask Queue: [callback1]

Microtask Queue: []

打印結果:
1

Step 3

new Promise((resolve, reject) => {
  // 注意,這裏是同步執行的,若是不太清楚,能夠去看一下我開頭本身實現的promise啦~~
  console.log(4)
  resolve(5)
}).then((data) => {
  // 這個回調函數叫作callback2,promise屬於microtask,因此放到microtask queue中
  console.log(data);
})
複製代碼

Stack Queue: [promise]

Macrotask Queue: [callback1]

Microtask Queue: [callback2]

打印結果:
1
4

Step 5

setTimeout(() => {
  // 這個回調函數叫作callback3,setTimeout屬於macrotask,因此放到macrotask queue中
  console.log(6);
})
複製代碼

Stack Queue: [setTimeout]

Macrotask Queue: [callback1, callback3]

Microtask Queue: [callback2]

打印結果:
1
4

Step 6

console.log(7)
複製代碼

Stack Queue: [console]

Macrotask Queue: [callback1, callback3]

Microtask Queue: [callback2]

打印結果:
1
4
7


  1. 好啦,全局Script代碼執行完了,進入下一個步驟,從microtask queue中依次取出任務執行,直到microtask queue隊列爲空。

Step 7

console.log(data)       // 這裏data是Promise的決議值5
複製代碼

Stack Queue: [callback2]

Macrotask Queue: [callback1, callback3]

Microtask Queue: []

打印結果:
1
4
7
5


  1. 這裏microtask queue中只有一個任務,執行完後開始從宏任務隊列macrotask queue中取位於隊首的任務執行

Step 8

console.log(2)
複製代碼

Stack Queue: [callback1]

Macrotask Queue: [callback3]

Microtask Queue: []

打印結果:
1
4
7
5
2

可是,執行callback1的時候又遇到了另外一個Promise,Promise異步執行完後在microtask queue中又註冊了一個callback4回調函數

Step 9

Promise.resolve().then(() => {
  // 這個回調函數叫作callback4,promise屬於microtask,因此放到microtask queue中
  console.log(3)
});
複製代碼

Stack Queue: [promise]

Macrotask v: [callback3]

Microtask Queue: [callback4]

打印結果:
1
4
7
5
2


  1. 取出一個宏任務macrotask執行完畢,而後再去微任務隊列microtask queue中依次取出執行

Step 10

console.log(3)
複製代碼

Stack Queue: [callback4]

Macrotask Queue: [callback3]

Microtask Queue: []

打印結果:
1
4
7
5
2
3


  1. 微任務隊列所有執行完,再去宏任務隊列中取第一個任務執行

Step 11

console.log(6)
複製代碼

Stack Queue: [callback3]

Macrotask Queue: []

Microtask Queue: []

打印結果:
1
4
7
5
2
3
6


  1. 以上,所有執行完後,Stack Queue爲空,Macrotask Queue爲空,Micro Queue爲空

Stack Queue: []

Macrotask Queue: []

Microtask Queue: []

最終打印結果:
1
4
7
5
2
3
6

由於是第一個例子,因此這裏分析的比較詳細,你們仔細看一下,接下來咱們再來一個例子:

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  console.log(data);
  
  Promise.resolve().then(() => {
    console.log(6)
  }).then(() => {
    console.log(7)
    
    setTimeout(() => {
      console.log(8)
    }, 0);
  });
})

setTimeout(() => {
  console.log(9);
})

console.log(10);

複製代碼

最終輸出結果是什麼呢?參考前面的例子,好好想想......

// 正確答案
1
4
10
5
6
7
2
3
9
8
複製代碼

相信你們都答對了,這裏的關鍵在前面已經提過:

在執行微隊列microtask queue中任務的時候,若是又產生了microtask,那麼會繼續添加到隊列的末尾,也會在這個週期執行,直到microtask queue爲空中止。

注:固然若是你在microtask中不斷的產生microtask,那麼其餘宏任務macrotask就沒法執行了,可是這個操做也不是無限的,拿NodeJS中的微任務process.nextTick()來講,它的上限是1000個,後面咱們會講到。

瀏覽器的Event Loop就說到這裏,下面咱們看一下NodeJS中的Event Loop,它更復雜一些,機制也不太同樣。

NodeJS中的Event Loop

libuv

先來看一張libuv的結構圖:

node-libuv

NodeJS中的宏隊列和微隊列

NodeJS的Event Loop中,執行宏隊列的回調任務有6個階段,以下圖:

node-eventloop-6phase

各個階段執行的任務以下:

  • timers階段:這個階段執行setTimeout和setInterval預約的callback
  • I/O callback階段:執行除了close事件的callbacks、被timers設定的callbacks、setImmediate()設定的callbacks這些以外的callbacks
  • idle, prepare階段:僅node內部使用
  • poll階段:獲取新的I/O事件,適當的條件下node將阻塞在這裏
  • check階段:執行setImmediate()設定的callbacks
  • close callbacks階段:執行socket.on('close', ....)這些callbacks

NodeJS中宏隊列主要有4個

由上面的介紹能夠看到,回調事件主要位於4個macrotask queue中:

  1. Timers Queue
  2. IO Callbacks Queue
  3. Check Queue
  4. Close Callbacks Queue

這4個都屬於宏隊列,可是在瀏覽器中,能夠認爲只有一個宏隊列,全部的macrotask都會被加到這一個宏隊列中,可是在NodeJS中,不一樣的macrotask會被放置在不一樣的宏隊列中。

NodeJS中微隊列主要有2個

  1. Next Tick Queue:是放置process.nextTick(callback)的回調任務的
  2. Other Micro Queue:放置其餘microtask,好比Promise等

在瀏覽器中,也能夠認爲只有一個微隊列,全部的microtask都會被加到這一個微隊列中,可是在NodeJS中,不一樣的microtask會被放置在不一樣的微隊列中。

具體能夠經過下圖加深一下理解:

node-eventloop

大致解釋一下NodeJS的Event Loop過程:

  1. 執行全局Script的同步代碼
  2. 執行microtask微任務,先執行全部Next Tick Queue中的全部任務,再執行Other Microtask Queue中的全部任務
  3. 開始執行macrotask宏任務,共6個階段,從第1個階段開始執行相應每個階段macrotask中的全部任務,注意,這裏是全部每一個階段宏任務隊列的全部任務,在瀏覽器的Event Loop中是隻取宏隊列的第一個任務出來執行,每個階段的macrotask任務執行完畢後,開始執行微任務,也就是步驟2
  4. Timers Queue -> 步驟2 -> I/O Queue -> 步驟2 -> Check Queue -> 步驟2 -> Close Callback Queue -> 步驟2 -> Timers Queue ......
  5. 這就是Node的Event Loop

關於NodeJS的macrotask queue和microtask queue,我畫了兩張圖,你們做爲參考:

node-microtaskqueue

node-macrotaskqueue

好啦,概念理解了咱們經過幾個例子來實戰一下:

第一個例子

console.log('start');

setTimeout(() => {          // callback1
  console.log(111);
  setTimeout(() => {        // callback2
    console.log(222);
  }, 0);
  setImmediate(() => {      // callback3
    console.log(333);
  })
  process.nextTick(() => {  // callback4
    console.log(444);  
  })
}, 0);

setImmediate(() => {        // callback5
  console.log(555);
  process.nextTick(() => {  // callback6
    console.log(666);  
  })
})

setTimeout(() => {          // callback7              
  console.log(777);
  process.nextTick(() => {  // callback8
    console.log(888);   
  })
}, 0);

process.nextTick(() => {    // callback9
  console.log(999);  
})

console.log('end');
複製代碼


更新 2018.9.20

上面這段代碼你執行的結果可能會有多種狀況,緣由解釋以下。

  • setTimeout(fn, 0)不是嚴格的0,通常是setTimeout(fn, 3)或什麼,會有必定的延遲時間,當setTimeout(fn, 0)和setImmediate(fn)出如今同一段同步代碼中時,就會存在兩種狀況。

  • 第1種狀況:同步代碼執行完了,Timer還沒到期,setImmediate回調先註冊到Check Queue中,開始執行微隊列,而後是宏隊列,先從Timers Queue中開始,發現沒回調,往下走直到Check Queue中有回調,執行,而後timer到期(只要在執行完Timer Queue後到期效果就都同樣),timer回調註冊到Timers Queue中,下一輪循環執行到Timers Queue中才能執行那個timer 回調;因此,這種狀況下,setImmediate(fn)回調先於setTimeout(fn, 0)回調執行

  • 第2種狀況:同步代碼還沒執行完,timer先到期,timer回調先註冊到Timers Queue中,執行到setImmediate了,它的回調再註冊到Check Queue中。 而後,同步代碼執行完了,執行微隊列,而後開始先執行Timers Queue,先執行Timer 回調,再到Check Queue,執行setImmediate回調;因此,這種狀況下,setTimeout(fn, 0)回調先於setImmediate(fn)回調執行

  • 因此,在同步代碼中同時調setTimeout(fn, 0)和setImmediate狀況是不肯定的,可是若是把他們放在一個IO的回調,好比readFile('xx', function () {// ....})回調中,那麼IO回調是在IO Queue中,setTimeout到期回調註冊到Timers Queue,setImmediate回調註冊到Check Queue,IO Queue執行完到Check Queue,timer Queue獲得下個週期,因此setImmediate回調這種狀況下確定比setTimeout(fn, 0)回調先執行。

綜上,這個例子是不太好的,setTimeout(fn, 0)和setImmediate(fn)若是想要保證結果惟一,就放在一個IO Callback中吧,上面那段代碼能夠把全部它倆同步執行的代碼都放在一個IO Callback中,結果就惟一了。

更新結束



請運用前面學到的知識,仔細分析一下......

// 正確答案
start
end
999
111
777
444
888
555
333
666
222
複製代碼

你答對了嗎?咱們來一塊兒分析一下:

  1. 執行全局Script代碼,先打印start,向下執行,將setTimeout的回調callback1註冊到Timers Queue中,再向下執行,將setImmediate的回調callback5註冊到Check Queue中,接着向下執行,將setTimeout的回調callback7註冊到Timers Queue中,繼續向下,將process.nextTick的回調callback9註冊到微隊列Next Tick Queue中,最後一步打印end。此時,各個隊列的回調狀況以下:

宏隊列

Timers Queue: [callback1, callback7]

Check Queue: [callback5]

IO Callback Queue: []

Close Callback Queue: []

微隊列

Next Tick Queue: [callback9]

Other Microtask Queue: []

打印結果
start
end

  1. 全局Script執行完了,開始依次執行微任務Next Tick Queue中的所有回調任務。此時Next Tick Queue中只有一個callback9,將其取出放入調用棧中執行,打印999

宏隊列

Timers Queue: [callback1, callback7]

Check Queue: [callback5]

IO Callback Queue: []

Close Callback Queue: []

微隊列

Next Tick Queue: []

Other Microtask Queue: []

打印結果
start
end
999

  1. 開始依次執行6個階段各自宏隊列中的全部任務,先執行第1個階段Timers Queue中的全部任務,先取出callback1執行,打印111,callback1函數繼續向下,依次把callback2放入Timers Queue中,把callback3放入Check Queue中,把callback4放入Next Tick Queue中,而後callback1執行完畢。再取出Timers Queue中此時排在首位的callback7執行,打印777,把callback8放入Next Tick Queue中,執行完畢。此時,各隊列狀況以下:

宏隊列

Timers Queue: [callback2]

Check Queue: [callback5, callback3]

IO Callback Queue: []

Close Callback Queue: []

微隊列

Next Tick Queue: [callback4, callback8]

Other Microtask Queue: []

打印結果
start
end
999
111
777

  1. 6個階段每階段的宏任務隊列執行完畢後,都會開始執行微任務,此時,先取出Next Tick Queue中的全部任務執行,callback4開始執行,打印444,而後callback8開始執行,打印888,Next Tick Queue執行完畢,開始執行Other Microtask Queue中的任務,由於裏面爲空,因此繼續向下。

宏隊列

Timers Queue: [callback2]

Check Queue: [callback5, callback3]

IO Callback Queue: []

Close Callback Queue: []

微隊列

Next Tick Queue: []

Other Microtask Queue: []

打印結果
start
end
999
111
777
444
888

  1. 第2個階段IO Callback Queue隊列爲空,跳過,第3和第4個階段通常是Node內部使用,跳過,進入第5個階段Check Queue。取出callback5執行,打印555,把callback6放入Next Tick Queue中,執行callback3,打印333

宏隊列

Timers Queue: [callback2]

Check Queue: []

IO Callback Queue: []

Close Callback Queue: []

微隊列

Next Tick Queue: [callback6]

Other Microtask Queue: []

打印結果
start
end
999
111
777
444
888
555
333

  1. 執行微任務隊列,先執行Next Tick Queue,取出callback6執行,打印666,執行完畢,由於Other Microtask Queue爲空,跳過。

宏隊列

Timers Queue: [callback2]

Check Queue: []

IO Callback Queue: []

Close Callback Queue: []

微隊列

Next Tick Queue: [callback6]

Other Microtask Queue: []

打印結果
start
end
999
111
777
444
888
555
333

  1. 執行第6個階段Close Callback Queue中的任務,爲空,跳過,好了,此時一個循環已經結束。進入下一個循環,執行第1個階段Timers Queue中的全部任務,取出callback2執行,打印222,完畢。此時,全部隊列包括宏任務隊列和微任務隊列都爲空,再也不打印任何東西。

宏隊列

Timers Queue: []

Check Queue: []

IO Callback Queue: []

Close Callback Queue: []

微隊列

Next Tick Queue: [callback6]

Other Microtask Queue: []

最終結果
start
end
999
111
777
444
888
555
333
666
222

以上就是這道題目的詳細分析,若是沒有明白,必定要多看幾回。


下面引入Promise再來看一個例子:

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')
    })
})

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

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
複製代碼

你們仔細分析,相比於上一個例子,這裏因爲存在Promise,因此Other Microtask Queue中也會有回調任務的存在,執行到微任務階段時,先執行Next Tick Queue中的全部任務,再執行Other Microtask Queue中的全部任務,而後纔會進入下一個階段的宏任務。明白了這一點,相信你們均可以分析出來,下面直接給出正確答案,若有疑問,歡迎留言和我討論。

// 正確答案
1
7
6
8
2
4
9
11
3
10
5
12
複製代碼

setTimeout 對比 setImmediate

  • setTimeout(fn, 0)在Timers階段執行,而且是在poll階段進行判斷是否達到指定的timer時間纔會執行
  • setImmediate(fn)在Check階段執行

二者的執行順序要根據當前的執行環境才能肯定:

  • 若是二者都在主模塊(main module)調用,那麼執行前後取決於進程性能,順序隨機
  • 若是二者都不在主模塊調用,即在一個I/O Circle中調用,那麼setImmediate的回調永遠先執行,由於會先到Check階段

setImmediate 對比 process.nextTick

  • setImmediate(fn)的回調任務會插入到宏隊列Check Queue中
  • process.nextTick(fn)的回調任務會插入到微隊列Next Tick Queue中
  • process.nextTick(fn)調用深度有限制,上限是1000,而setImmedaite則沒有

總結

  1. 瀏覽器的Event Loop和NodeJS的Event Loop是不一樣的,實現機制也不同,不要混爲一談。
  2. NodeJS能夠理解成有4個宏任務隊列和2個微任務隊列,可是執行宏任務時有6個階段。先執行全局Script代碼,執行完同步代碼調用棧清空後,先從微任務隊列Next Tick Queue中依次取出全部的任務放入調用棧中執行,再從微任務隊列Other Microtask Queue中依次取出全部的任務放入調用棧中執行。而後開始宏任務的6個階段,每一個階段都將該宏任務隊列中的全部任務都取出來執行(注意,這裏和瀏覽器不同,瀏覽器只取一個),每一個宏任務階段執行完畢後,開始執行微任務,再開始執行下一階段宏任務,以此構成事件循環。
  3. NodeJS能夠理解成有4個宏任務隊列和2個微任務隊列,可是執行宏任務時有6個階段。先執行全局Script代碼,執行完同步代碼調用棧清空後,先從微任務隊列Next Tick Queue中依次取出全部的任務放入調用棧中執行,再從微任務隊列Other Microtask Queue中依次取出全部的任務放入調用棧中執行。而後開始宏任務的6個階段,每一個階段都將該宏任務隊列中的全部任務都取出來執行(注意,這裏和瀏覽器不同,瀏覽器只取一個),6個階段執行完畢後,再開始執行微任務,以此構成事件循環。
  4. MacroTask包括: setTimeout、setInterval、 setImmediate(Node)、requestAnimation(瀏覽器)、IO、UI rendering
  5. Microtask包括: process.nextTick(Node)、Promise、Object.observe、MutationObserver

歡迎關注個人公衆號

微信公衆號

參考連接

不要混淆nodejs和瀏覽器中的event loop

node中的Event模塊

Promises, process.nextTick And setImmediate

瀏覽器和Node不一樣的事件循環

Tasks, microtasks, queues and schedules

理解事件循環淺析

相關文章
相關標籤/搜索