本文以 Node.js 爲例,講解 Event Loop 在 Node.js 的實現,原文,JavaScript 中的實現大同小異。javascript
單線程的 Node.js 可以實現無阻塞IO
的緣由就是事件循環(Event Loop)。java
如今大多數系統內核是多線程的,因此它們能夠在後臺執行多個操做,當這些操做完成時,內核就會通知 Node.js,而這些操做的回調函數被添加到事件輪詢列表(poll queue),而且 Node.js 會在適當的時機執行回調函數。node
當 Node.js 開始執行時,便初始化 Event Loop,執行過程當中會存在許多異步操做,如:REPL、定時器(timers)、調用異步 API(請求,事件監聽),在主進程代碼執行完後,便開始運行 Event Loop
。git
下圖描述了 Event Loop 中的各個階段github
┌───────────────────────┐ ┌─>│ timers │ 這個階段執行 `setTimeout()` 和 `setInterval()` 中的回調函數 │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ 這個階段執行除了 `close` 回調函數之外的幾乎全部的 I/0 回調函數 │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ 這個階段僅僅 Node.js 內部使用 │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ 執行隊列中的回調函數、檢索新的回調函數 │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ `setImmediate()` 將在這裏被調用 │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ `close` 回調函數被調用如:socket.on('close', ...) └───────────────────────┘
setTimeout() 和 setInterval() 都要指定一個運行時間,這個運行時間其實不是確切的運行時間,而是一個指望時間,Event Loop 會在 timers 階段執行超過時望時間的定時器回調函數,但因爲你不肯定在其餘階段甚至主進程中的事件執行時間,因此定時器不必定會按時執行。多線程
var asyncApi = function (callback) { setTimeout(callback, 90) } const timeoutScheduled = Date.now(); setTimeout(() => { const delay = Date.now() - timeoutScheduled; console.log(`${delay}ms setTimeout 被執行`); // 140ms 以後被執行 }, 100); asyncApi(() => { const startCallback = Date.now(); while (Date.now() - startCallback < 50) { // do nothing } })
這個階段主要執行一些系統操做帶來的回調函數,如 TCP 錯誤,若是 TCP 嘗試連接時出現 ECONNREFUSED
錯誤 ,一些 *nix 會把這個錯誤報告給 Node.js。而這個錯誤報告會先進入隊列中,而後在 I/O callbacks 階段執行。異步
poll 階段有兩個主要功能:socket
也會執行時間定時器到達指望時間的回調函數async
執行事件循環列表(poll queue)裏的函數ide
當 Event Loop 進入 poll 階段而且沒有其他的定時器,那麼:
若是事件循環列表不爲空
,則迭代同步的執行隊列中的函數。
若是事件循環列表爲空
,則判斷是否有 setImmediate()
函數待執行。若是有結束 poll
階段,直接到
check
階段。若是沒有,則等待回調函數進入隊列並當即執行。
在 poll 階段結束以後,執行 setImmediate()
。
忽然結束的事件的回調函數會在這裏觸發,若是 socket.destroy()
,那麼 close
會被觸發在這個階段,也有可能經過 process.nextTick()
來觸發。
這裏要說明一下 process.nextTick()
是在下次事件循環以前運行,若是把 process.nextTick()
和 setImmediate()
寫在一塊兒,那麼是 process.nextTick()
先執行。next
比 immediate
快,官方也說這個函數命名有問題,可是由於歷史存留沒辦法解決。
process.nextTick(() => { console.log('nextTick'); }); setImmediate(() => { console.log('setImmediate'); }); setTimeout(() => { console.log('setTimeout'); }, 0) // 執行結果,nextTick, setTimeout, setImmediate // 查看 Node.js 源碼,setTimeout(fun, 0) 會轉化成 setTimeout(fun, 1),因此在這種簡單的狀況下,對於不一樣設備,setImmediate 有可能早於 setTimeout 執行。
理解事件循環,會知道 JavaScript 如何無阻塞運行的,以及它簡潔的開發思路和事件驅動風格。
做者:肖沐宸,github。