Node.js 是基於V8引擎的javascript運行環境. Node.js具備
事件驅動
,非阻塞I/O
等特色. 結合Node API, Node.js 具備網絡編程, 文件系統等服務端的功能, Node.js用libuv
庫進行異步事件處理.javascript
Node.js的單線程含義, 實際上說的是執行同步代碼的主線程. 一個Node程序的啓動, 不止是分配了一個線程,而是咱們只能在一個線程執行代碼. 當出現I/O資源調用, TCP鏈接等外部資源申請的時候, 不會阻塞主線程, 而是委託給I/O線程進行處理,而且進入等待隊列. 一旦主線程執行完成,將會消費事件隊列(Event Queue). 由於只有一個主線程, 只佔用CPU內核處理邏輯計算, 所以不適合在CPU密集型進行使用.html
注意,上圖的EVENT_QUEUE
給人看起來是隻有一個隊列, 根據Node.js官方介紹, EventLoop
有6個階段, 同時每一個階段都有對應的一個先進先出的回調隊列. java
In computer science, the event loop, message dispatcher, message loop, message pump, or run loop is a programming construct that waits for and dispatches events or messages in a program. -- from wikinode
大概含義: EventLoop 是一種經常使用的機制,經過對內部或外部的事件提供者發出請求, 如文件讀寫, 網絡鏈接 等異步操做, 完成後調用事件處理程序. 整個過程都是異步階段git
When Node.js starts, it initializes the event loop, processes the provided input script (or drops into the REPL, which is not covered in this document) which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop. -- from node.js docgithub
大體含義: 當Node.js 啓動, 就會初始化一個 event loop, 處理腳本時, 可能會發生異步API行爲調用, 使用定時器任務或者nexTick, 處理完成後進入事件循環處理過程編程
┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘
每個階段都有一個FIFO的callbacks隊列, 每一個階段都有本身的事件處理方式. 當事件循環進入某個階段時, 將會在該階段內執行回調,直到隊列耗盡或者回調的最大數量已執行, 那麼將進入下一個處理階段. 網絡
下面是摘抄creeperyang 對上面6個階段的 (原文翻譯)異步
一個timer指定一個下限時間而不是準確時間,在達到這個下限時間後執行回調。在指定時間事後,timers會盡量早地執行回調,但系統調度或者其它回調的執行可能會延遲它們。socket
注意:技術上來講,poll 階段控制 timers 何時執行。
注意:這個下限時間有個範圍:[1, 2147483647],若是設定的時間不在這個範圍,將被設置爲1。
這個階段執行一些系統操做的回調。好比TCP錯誤,如一個TCP socket在想要鏈接時收到ECONNREFUSED,
類unix系統會等待以報告錯誤,這就會放到 I/O callbacks 階段的隊列執行.
名字會讓人誤解爲執行I/O回調處理程序, 實際上I/O回調會由poll階段處理.
poll 階段有兩個主要功能:
執行下限時間已經達到的timers的回調,而後
處理 poll 隊列裏的事件。
當event loop進入 poll 階段,而且 沒有設定的timers(there are no timers scheduled),會發生下面兩件事之一:
若是 poll 隊列不空,event loop會遍歷隊列並同步執行回調,直到隊列清空或執行的回調數到達系統上限;
若是 poll 隊列爲空,則發生如下兩件事之一:
可是,當event loop進入 poll 階段,而且 有設定的timers,一旦 poll 隊列爲空(poll 階段空閒狀態):
這個階段容許在 poll 階段結束後當即執行回調。若是 poll 階段空閒,而且有被setImmediate()設定的回調,event loop會轉到 check 階段而不是繼續等待。
setImmediate()其實是一個特殊的timer,跑在event loop中一個獨立的階段。它使用libuv的API
來設定在 poll 階段結束後當即執行回調。
一般上來說,隨着代碼執行,event loop終將進入 poll 階段,在這個階段等待 incoming connection, request 等等。可是,只要有被setImmediate()設定了回調,一旦 poll 階段空閒,那麼程序將結束 poll 階段並進入 check 階段,而不是繼續等待 poll 事件們 (poll events)。
若是一個 socket 或 handle 被忽然關掉(好比 socket.destroy()),close事件將在這個階段被觸發,不然將經過process.nextTick()觸發
簡單的 EventLoop
const fs = require('fs'); let counts = 0; function wait (mstime) { let date = Date.now(); while (Date.now() - date < mstime) { // do nothing } } function asyncOperation (callback) { fs.readFile(__dirname + '/' + __filename, callback); } const lastTime = Date.now(); setTimeout(() => { console.log('timers', Date.now() - lastTime + 'ms'); }, 0); process.nextTick(() => { // 進入event loop // timers階段以前執行 wait(20); asyncOperation(() => { console.log('poll'); }); }); /** * result: * timers 21ms * poll */
爲了讓setTimeout
優先於fs.readFile
回調, 執行了process.nextTick, 表示在進入 timers階段前, 等待20ms後執行文件讀取.
process.nextTick 不屬於事件循環的任何一個階段,它屬於該階段與下階段之間的過渡, 即本階段執行結束, 進入下一個階段前, 所要執行的回調。有給人一種插隊的感受.
setImmediate的回調處於check階段, 當poll階段的隊列爲空, 且check階段的事件隊列存在的時候,切換到check階段執行.
nextTick 遞歸的危害
因爲nextTick具備插隊的機制,nextTick的遞歸會讓事件循環機制沒法進入下一個階段. 致使I/O處理完成或者定時任務超時後仍然沒法執行, 致使了其它事件處理程序處於飢餓狀態. 爲了防止遞歸產生的問題, Node.js 提供了一個 process.maxTickDepth (默認 1000)。
遞歸nextTick
const fs = require('fs'); let counts = 0; function wait (mstime) { let date = Date.now(); while (Date.now() - date < mstime) { // do nothing } } function nextTick () { process.nextTick(() => { wait(20); nextTick(); }); } const lastTime = Date.now(); setTimeout(() => { console.log('timers', Date.now() - lastTime + 'ms'); }, 0); nextTick();
此時永遠沒法跳到timer階段, 由於在進入timers階段前有不斷的nextTick插入執行. 除非執行了1000次到了執行上限.
setImmediate
若是在一個I/O週期內進行調度,setImmediate()將始終在任何定時器以前執行.
無 I/O 處理狀況下
setTimeout(function timeout () { console.log('timeout'); },0); setImmediate(function immediate () { console.log('immediate'); });
輸出結果是 不肯定 的!
setTimeout(fn, 0) 具備幾毫秒的不肯定性. 沒法保證進入timers階段, 定時器可以當即執行處理程序.
在I/O事件處理程序下
var fs = require('fs') fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') }) })
此時 setImmediate
優先於 setTimeout
執行,由於 poll階段執行完成後 進入 check階段. timers階段處於下一個事件循環階段了.