瀏覽器和Node不一樣的事件循環(Event Loop)

注意

在 Node 11 版本中,Node 的 Event Loop 已經與 瀏覽器趨於相同。html

背景

Event Loop也是js老生常談的一個話題了。2月底看了阮一峯老師的《Node定時器詳解》一文後,發現沒法徹底對標以前看過的js事件循環執行機制,又查閱了一些其餘資料,記爲筆記,感受不妥,總結成文。html5

瀏覽器中與node中事件循環與執行機制不一樣,不可混爲一談。 瀏覽器的Event loop是在HTML5中定義的規範,而node中則由libuv庫實現。同時閱讀《深刻淺出nodeJs》一書時發現比較當時node機制已有不一樣,因此本文node部分針對爲此文發佈時版本。強烈推薦讀下參考連接中的前三篇。node

瀏覽器環境

js執行爲單線程(不考慮web worker),全部代碼皆在主線程調用棧完成執行。當主線程任務清空後纔會去輪詢取任務隊列中任務。git

任務隊列

異步任務分爲task(宏任務,也可稱爲macroTask)和microtask(微任務)兩類。 當知足執行條件時,task和microtask會被放入各自的隊列中等待放入主線程執行,咱們把這兩個隊列稱爲Task Queue(也叫Macrotask Queue)和Microtask Queue。github

  • task:script中代碼、setTimeout、setInterval、I/O、UI render。
  • microtask: promise、Object.observe、MutationObserver。

具體過程

  1. 執行完主執行線程中的任務。
  2. 取出Microtask Queue中任務執行直到清空。
  3. 取出Macrotask Queue中一個任務執行。
  4. 取出Microtask Queue中任務執行直到清空。
  5. 重複3和4。

即爲同步完成,一個宏任務,全部微任務,一個宏任務,全部微任務......web

注意

  • 在瀏覽器頁面中能夠認爲初始執行線程中沒有代碼,每個script標籤中的代碼是一個獨立的task,即會執行完前面的script中建立的microtask再執行後面的script中的同步代碼。
  • 若是microtask一直被添加,則會繼續執行microtask,「卡死」macrotask。
  • 部分版本瀏覽器有執行順序與上述不符的狀況,多是不符合標準或js與html部分標準衝突。可閱讀參考文章中第一篇。
  • new Promise((resolve, reject) =>{console.log(‘同步’);resolve()}).then(() => {console.log('異步')}),即promisethencatch纔是microtask,自己的內部代碼不是。
  • 個別瀏覽器獨有API未列出。

僞代碼

while (true) {
  宏任務隊列.shift()
  微任務隊列所有任務()
}
複製代碼

node環境

js執行爲單線程,全部代碼皆在主線程調用棧完成執行。當主線程任務清空後纔會去輪詢取任務隊列中任務。api

循環階段

在node中事件每一輪循環按照順序分爲6個階段,來自libuv的實現:promise

  1. timers:執行知足條件的setTimeout、setInterval回調。
  2. I/O callbacks:是否有已完成的I/O操做的回調函數,來自上一輪的poll殘留。
  3. idle,prepare:可忽略
  4. poll:等待還沒完成的I/O事件,會因timers和超時時間等結束等待。
  5. check:執行setImmediate的回調。
  6. close callbacks:關閉全部的closing handles,一些onclose事件。

執行機制

幾個隊列

除上述循環階段中的任務類型,咱們還剩下瀏覽器和node共有的microtask和node獨有的process.nextTick,咱們稱之爲Microtask Queue和NextTick Queue。瀏覽器

咱們把循環中的幾個階段的執行隊列也分別稱爲Timers Queue、I/O Queue、Check Queue、Close Queue。bash

循環以前

在進入第一次循環以前,會先進行以下操做:

  • 同步任務
  • 發出異步請求
  • 規劃定時器生效的時間
  • 執行process.nextTick()

開始循環

按照咱們的循環的6個階段依次執行,每次拿出當前階段中的所有任務執行,清空NextTick Queue,清空Microtask Queue。再執行下一階段,所有6個階段執行完畢後,進入下輪循環。即:

  • 清空當前循環內的Timers Queue,清空NextTick Queue,清空Microtask Queue。
  • 清空當前循環內的I/O Queue,清空NextTick Queue,清空Microtask Queue。
  • 清空當前循環內的Check Queu,清空NextTick Queue,清空Microtask Queue。
  • 清空當前循環內的Close Queu,清空NextTick Queue,清空Microtask Queue。
  • 進入下輪循環。

能夠看出,nextTick優先級比promise等microtask高。setTimeoutsetInterval優先級比setImmediate高。

注意

  • 若是在timers階段執行時建立了setImmediate則會在此輪循環的check階段執行,若是在timers階段建立了setTimeout,因爲timers已取出完畢,則會進入下輪循環,check階段建立timers任務同理。
  • setTimeout優先級比setImmediate高,可是因爲setTimeout(fn,0)的真正延遲不可能徹底爲0秒,可能出現先建立的setTimeout(fn,0)而比setImmediate的回調後執行的狀況。

僞代碼

while (true) {
  loop.forEach((階段) => {
    階段所有任務()
    nextTick所有任務()
    microTask所有任務()
  })
  loop = loop.next
}
複製代碼

測試代碼

function sleep(time) {
  let startTime = new Date()
  while (new Date() - startTime < time) {}
  console.log('1s over')
}
setTimeout(() => {
  console.log('setTimeout - 1')
  setTimeout(() => {
      console.log('setTimeout - 1 - 1')
      sleep(1000)
  })
  new Promise(resolve => resolve()).then(() => {
      console.log('setTimeout - 1 - then')
      new Promise(resolve => resolve()).then(() => {
          console.log('setTimeout - 1 - then - then')
      })
  })
  sleep(1000)
})

setTimeout(() => {
  console.log('setTimeout - 2')
  setTimeout(() => {
      console.log('setTimeout - 2 - 1')
      sleep(1000)
  })
  new Promise(resolve => resolve()).then(() => {
      console.log('setTimeout - 2 - then')
      new Promise(resolve => resolve()).then(() => {
          console.log('setTimeout - 2 - then - then')
      })
  })
  sleep(1000)
})
複製代碼
  • 瀏覽器輸出:
    setTimeout - 1 //1爲單個task
    1s over
    setTimeout - 1 - then
    setTimeout - 1 - then - then 
    setTimeout - 2 //2爲單個task
    1s over
    setTimeout - 2 - then
    setTimeout - 2 - then - then
    setTimeout - 1 - 1
    1s over
    setTimeout - 2 - 1
    1s over
    複製代碼
  • node輸出:
    setTimeout - 1 
    1s over
    setTimeout - 2 //一、2爲單階段task
    1s over
    setTimeout - 1 - then
    setTimeout - 2 - then
    setTimeout - 1 - then - then
    setTimeout - 2 - then - then
    setTimeout - 1 - 1
    1s over
    setTimeout - 2 - 1
    1s over
    複製代碼

由此也可看出事件循環在瀏覽器和node中的不一樣。

因爲新版 node 執行狀況與瀏覽器相同,因此瀏覽器環境爲例,以 console 輸出值代指值所在函數,執行過程以下

<!--執行完主執行線程中的任務。-->
<!--取出Microtask Queue中任務執行直到清空。-->
<!--取出Macrotask Queue中一個任務執行。-->
<!--取出Microtask Queue中任務執行直到清空。-->
<!--重複3和4。-->
以 IQ 代指微任務隊列,AQ 代指宏任務隊列
1. 執行完主線程中任務:主執行線程執行完畢,setTimeout-一、setTimeout-2 進入等待
2. 清空 IQ:此時 IQ 中無任務
2. 執行 AQ 中一個任務: setTimeout-1 到時間後進入 AQ 中,被執行,執行過程當中 setTimeout-1-1 進入等待狀態,setTimeout-1-then 直接進入 IQ 隊列,因爲 setTimeout-1 中有 1s 等待,此時 setTimeout-2 確定已經進入 AQ,setTimeout-1-1 也隨後進入 AQ,此時結束狀態爲 IQ: [setTimeout-1-then],AQ: [setTimeout-2, setTimeout-1-1]
3. 清空 IQ: 此時 IQ 中有 setTimeout-1-then,執行 setTimeout-1-then,執行過程當中,setTimout-1-then-then 直接被加入 IQ,因此 IQ 沒清空,因此繼續執行 setTimout-1-then-then,IQ 被清空,此時結束狀態爲 IQ: [], AQ:  [setTimeout-2, setTimeout-1-1]
4. 執行 AQ 中一個任務:即執行 setTimeout-2
5. 清空 IQ: 這一步與 3 類似,因此輸出 setTimeout-2-then、setTimeout-2-then-then,IQ 清空,此時結束狀態爲 IQ: [], AQ: [setTimeout-1-1, setTimeout-2-1]
6. 執行 AQ 中一個任務:即 setTimeout-1-1
7. 清空 IQ: 自己就爲空
8. 執行 AQ 中一個任務:即 setTimeout-2-1
複製代碼

參考文章

相關文章
相關標籤/搜索