瞭解Event Loop

Event Loop事件循環簡介

JavaScript 是單線程的,因爲單線程會形成I/O阻塞,好比發送請求時未響應就可能形成頁面停滯,爲了解決這個問題,瀏覽器開始支持異步JS,異步JS就是把一些異步任務(ajax、定時器)等放到任務隊列中,而後經過事件循環不斷讀取、觸發任務隊列中的異步代碼,這種機制就叫作事件循環Event Loop。node

Event Loop的核心代碼是採用c++寫的(屬於NodeJs的範疇),本質上來講Event Loop就是採用輪詢的方式不斷讀取、執行事件,今天咱們要討論的就是事件循環中的細節。c++

階段

Event Loop內部分爲如下階段面試

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘複製代碼

上面的每個階段都有一個隊列(先進先出),裏面存放回調函數。每當Event Loop到達一個階段,通常來講都會執行隊列中的某些函數(也有可能不操做)ajax

各階段概覽

  • timers 階段:這個階段執行 setTimeout 和 setInterval 的回調函數。
  • I/O callbacks 階段:不在 timers 階段、close callbacks 階段和 check 階段這三個階段執行的回調,都由此階段負責,這幾乎包含了全部回調函數。
  • idle, prepare 階段(看起來是兩個階段,不過這不重要):event loop 內部使用的階段(咱們不用關心這個階段)
  • poll 階段:獲取新的 I/O 事件。在某些場景下 Node.js 會阻塞在這個階段。
  • check 階段:執行 setImmediate() 的回調函數。
  • close callbacks 階段:執行關閉事件的回調函數,如 socket.on('close', fn) 裏的 fn。

一個 Node.js 程序結束時,Node.js 會檢查 event loop 是否在等待異步 I/O 操做結束,是否在等待計時器觸發,若是沒有,就會關掉 event loop。promise

timers

這個階段頗有多是Event Loop開始的第一個階段,主要存放setTimeout或者setInterval等宏任務瀏覽器

poll

這個階段主要用來獲取新的I/O事件,當 event loop 進入 poll 階段,發現 poll 隊列爲空,event loop 檢查了一下最近的計時器,大概還有 100 毫秒時間,因而 event loop 決定這段時間就停在 poll 階段,當定時器任務快開始的時候,Event Loop會繞過poll階段進入check階段異步

check

這個階段有一個API,面試的時候常常用的到,屬於nodeJS的setImmediate,它一樣屬於宏任務,可是相對於定時器來講,它的特色是要求更快執行socket

setImmediate和setTimeout

setImmediate 和 setTimeout 很類似,可是其回調函數的調用時機卻不同。async

從所屬的階段隊列來看,setImmediate屬於check階段,setTimeout屬於timers階段,那麼二者之間到底誰先執行呢?ide

先看一段代碼

setTimeout(()=>{console.log('timeout')},0)
setImmediate(()=>{console.log('immediate')},0)複製代碼

通常來講,都會優先執行setTmmediate,可是上面的代碼實際執行順序是這樣的爲何會有時先執行timeout,有時先執行immediate呢,這要從順序看起,若是上面的代碼setTimeout的時間設定爲1000ms,那你們必定不會感到困惑,因爲immediate存在於check階段,當時間設定爲1000ms時,Event Loop處於poll階段,畢竟要等到1000ms才執行timers隊列中的函數,因此Loop打算休息一下。

而後呢好像時間差很少了,Loop發現check階段有個immediate函數,因而跑過去執行一下,執行完了就再跑到timers階段執行。

而上面產生困惑的最大緣由是定時器設置時間爲0,這就要看Event Loop開始時,所處的階段。

若是Event Loop此時在timers階段,隊列中尚未定時器任務,又或者定時器任務還沒到時間,那麼必然會跳過此階段,優先執行immediate任務。

若是此時有任務,並且時間到了,那麼必然會先執行setTimeout,這也是上述代碼產生困惑的緣由。

下面咱們對它進行改寫

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

  setImmediate(() => {console.log("immediate");
  });
}, 1000);複製代碼

上面的代碼,1秒後,執行箭頭函數,此時Event Loop並不在timers階段,因爲順序是不可變的,因此老是會優先執行immediate

process.nextTick()

你可能發現 process.nextTick() 這個重要的異步 API 沒有出如今任何一個階段裏,那是由於從技術上來說 process.nextTick() 並非 event loop 的一部分。實際上,無論 event loop 當前處於哪一個階段,nextTick 隊列都是在當前階段後就被執行了。

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

  setImmediate(() => {console.log("immediate");
  });
  
  process.nextTick(()=>{     console.log('nexTick')
  })
}, 1000);複製代碼

上面的代碼執行順序是這樣的

nextTick是在當前階段立刻執行,因爲上面的代碼執行後Loop處於poll階段,因此會優先執行nextTick

爲了更好得實驗,咱們再改一下代碼

setTimeout(() => {  setTimeout(() => {console.log("timeout");
    process.nextTick(() => {      console.log("nexTick2");
    });
  }, 0);

  setImmediate(() => {console.log("immediate");
  });

  process.nextTick(() => {console.log("nexTick");
  });
}, 1000);複製代碼

下面是結果,能夠發現nextTick是在當前階段立刻執行的

nexTick
immediate
timeout
nexTick2複製代碼
process.nextTick() 和 setImmediate()

這兩個函數功能很像,並且名字也很使人疑惑。

process.nextTick() 的回調會在當前 event loop 階段「當即」執行。 setImmediate() 的回調會在後續的 event loop 週期(tick)執行。

兩者的名字應該互換纔對。process.nextTick() 比 setImmediate() 更 immediate(當即)一些。

這是一個歷史遺留問題,並且爲了保證向後兼容性,也不太可能獲得改善。因此就算這兩個名字聽起來讓人很疑惑,也不會在將來有任何變化。

咱們推薦開發者在任何狀況下都使用 setImmediate(),由於它的兼容性更好,並且它更容易理解。

宏任務和微任務

異步任務中分宏任務和微任務,微任務老是比宏任務先執行

常見宏任務

常見微任務

經典面試題

setTimeout(()=> console.log(4))//宏任務new Promise(resolve => {
  resolve()//同步任務
  console.log(1) //同步任務}).then(()=> {  console.log(3) //微任務})console.log(2) //同步任務複製代碼

改形成await

setTimeout(_ => console.log(4)) //宏任務async function main() {  console.log(1) //同步任務
  await Promise.resolve() //同步任務 至關於 resolve()
  console.log(3) //至關於promise.then //微任務}
main()console.log(2) //同步任務複製代碼
相關文章
相關標籤/搜索