Skip to main content

事件循環

https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ https://wuch886.gitbooks.io/front-end-handbook/content/chapter1.html https://www.digitalocean.com/community/tutorials/how-to-use-multithreading-in-node-js#understanding-hidden-threads-in-node-js

事件循環的六個階段

NodeJS 中的事件循環和瀏覽器中的是完全不相同的東西。NodeJS 使用 V8 作為js的解析引擎,而 I/O 處理方面使用了自己設計的 libuv,libuv 是一個基於事件驅動的跨平台抽象層,封裝了不同操作系統一些底層特性,對外提供統一的 API,事件循環機制也是它裡面的實現。

libuv 引擎中的事件循環分為 6 個階段,它們會按照順序反覆運行。每當進入某一個階段的時候,都會從對應的回調隊列中取出函數去執行。當隊列為空或者執行的回調函數數量到達系統設定的閥值,就會進入下一個階段。

  1. V8 引擎解析JS腳本
  2. 解析後的代碼,調用NodeJS API
  3. libuv 庫負責 Node API的執行。他將不同的任務分配給不同的線程,形成一個 Event Loop(事件循環),以非同步方式將任務的執行結果返回給V8引擎。
  4. V8 引擎再將結果返回给用户。

Cache Flow

從上圖中,大致看出 NodeJS 中事件循環的順序:外部輸入數據 -> 輪詢階段 -> 檢查階段 -> 關閉事件回調階段 -> 定時器檢查階段 -> I/O事件回調階段 -> 閒置階段 -> 輪詢階段,按照順序反覆運行。

  1. timers - 定時器階段,處理 setTimeout 和 setInterval 的回調函數。進入這個階段後,主線程檢查當前時間,是否滿足定時器的條件。如果滿足就執行回調函數,否則就離開這個階段。timers 階段會執行 setTimeout 和 setInterval 回調,並且是由 poll 階段控制。同樣,在 NodeJS 中定時器指定的時間也不是準確時間,只能是儘快執行。
  2. I/O callbacks - 處理一些上一輪循環中的少數未執行的 I/O 回調。除了一下操作的回調函數,其他的回調函數都在這個階段執行。
  3. idle, prepare - 這階段只供 libuv 內部調用。
  4. Poll - 這個階段是輪循時間,用於等待還未返回的 I/O 事件,比如服務器的回應、用戶移動鼠標等等。獲取新的 I/O 事件,適當條件下 NodeJS 將阻塞在這裡。這個階段的時間會比較長,如果沒有其他非同步的任務要處理(例如到期的定時器),會一直停留在這個階段,等待 I/O 請求返回結果。poll 是一個至關重要的階段,這階段中,系統會做兩件事(1)回到 timer 階段執行回調(2)執行 I/O 回調,並且在進入該階段時間,如果沒有設定 timer 的話,會發生以下兩件事:(1)如果 poll 隊列不為空,會遍歷回調隊列並同步執行,直到隊列為空或者達到系統控制。(2)如果 poll 隊列為空時,會有兩件事發生,1. 如果有 setImmediate 回調需要執行,poll 階段會停止並且進入到 check 階段執行回調;2. 如果沒有 setImmediate 回調需要執行,會等待回調被加入到隊列中並立即執行回調,這裡同樣會有個超時時間設置防止一直等待下去,若設定了 timer 到話且 poll 隊列為空,則會判斷是否有 timer 超時,如果有的話回到 timer 階段執行回調。
  5. check - 該階段執行 setImmediate 的回調函數。
  6. close callbacks - 該階段執行關閉請求的回調函數,比如 socket.on('close', ...)。

Promise & async/await

Promise 的裡是同步程式碼,then 之後的程式碼會進入 micro queue;async 函數遇到 await 之前的程式碼是同步,遇到 await 執行後面函數,返回一個 promise,並把 await 以下的程式碼放入 micro queue。

new Promise((resolve, reject) => {
console.log ('new promise')
resolve ('succcess')
}).then ((res) => {
console.log ('promise done')
})

Cache Flow

NodeJS 隱藏線程

JavaScript 主線程,libuv 提供額外4個線程處理硬盤 I/O 和網絡 I/O,I/O 操作完成時,事件循環會在微任務隊列中新增與 I/O 任務關聯的回呼,主執行緒中的呼叫堆疊清空時,回呼將被推送到呼叫堆疊上,然後執行。另外,V8 引擎提供2個線程處理自動垃圾收集。

一個 NodeJS 進程中的線程總數達到 7 個:1 個主線程、4 個 I/O 線程和 2 個 V8 線程。

setTimeout 和 setImmediate

二者非常相似,區別主要在於調用時機不同。

  • setImmediate 設計在 poll 階段完成時執行,即 check 階段。
  • setTimeout 設計在 poll 階段為空閑時執行,且設定時間到達後執行,但它在 timer 階段執行。
  • I/O callback 調用則 setImmediate 永遠先執行,再執行 setTimeout。
  • process.nextTick 函數其實是獨立於 Event Loop 之外的,有一個自己的隊列,如果 nextTick 隊列不為空,就會清空隊列中的所有回調函數,並且優先於其他 microtask 執行。

瀏覽器和 NodeJS 事件循環

瀏覽器環境下,microtask 的任務隊列是每個 macrotask 執行完後執行。而在 NodeJS 中,microtask 會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行 microtask 隊列的任務。如果是 NodeJS 11 以上版本,一旦執行完一個階段裡的一個宏任務(setTimeout, setInterval 和 setImmediate) 就立刻執行微任務的任務,這就跟瀏覽器運行一致。

非同步I/O

node.js 的非同步機制是基於事件的,所有的磁盤 I/O、網絡I/O、數據庫查詢都以非阻塞方式執行,返回結果由事件循環處理,盡量讓耗時的 I/O 等操作併發執行。