本文,將會詳細的講解 node.js 事件循環工做流程和生命週期node
最多見的誤解之一,事件循環是 Javascript 引擎(V8,spiderMonkey等)的一部分。事實上事件循環主要利用 Javascript 引擎來執行代碼。api
首先沒有棧,其次這個過程是複雜的,有多個隊列(像數據結構中的隊列)參與。可是大多數開發者知道多少有的回調函數被推動一個單一的隊列裏面,是徹底錯誤的。數據結構
因爲錯誤的 node.js 事件循環圖,咱們中有一部分人認爲u有兩個線程。一個執行 Javascript,另外一個執行事件循環。事實上都在一個線程裏面運行。架構
另外一個很是大的誤解是 setTimeout 的回調函數在給定的延遲完成以後被(多是 OS 或者 內核)推動一個隊列。異步
做爲常見的事件循環描述只有一個隊列;因此一些開發者認爲 setImmediate 將回調放在工做隊列的前面。這是徹底錯誤的,在 Javascript 的工做隊列都是先進先出的。socket
在咱們開始描述事件循環的工做流程時,知道它的架構很是重要。下圖爲事件循環真正的工做流程:tcp
圖中不一樣的盒子表明不一樣的階段,每一個階段執行特定的工做。每一個階段都有一個隊列(這裏說成隊列主要是爲了更好理解;真實的數據結構可能不是隊列),Javascript 能夠在任何一個階段執行(除了 idle & prepare)。你在圖片中也能看到 nextTickQueue 和 microTaskQueue,它們不是循環的一部分,它們之中的回調能夠在任意階段執行。它們有更高的優先級去執行。ide
如今你知道了事件循環是不一樣階段和不一樣隊列的結合;下面是每一個階段的描述。函數
這個是事件循環開始的階段,綁定到這個階段的隊列,保留着定時器(setTimeout, setInterval)的回調,儘管它並無將回調推入隊列中,可是以最小的堆來維持計時器而且在到達規定的事件後執行回調。oop
這個階段執行在事件循環中 pending_queue 裏的回調,這些回調時被以前的操做推入的。例如當你嘗試往 tcp 中寫入一些東西,這個工做完成了,而後回調被推入到隊列中。錯誤處理的回調也在這裏。
儘管名字是空閒(idle),可是每一個 tick 都運行。Prepare 也在輪詢階段開始以前運行。無論怎樣,這兩個階段是 node 主要作一些內部操做的階段。
可能整個事件循環最重要的一個階段就是 poll phase。這個階段接受新傳入的鏈接(新的 Socket 創建等)和數據(文件讀取等)。咱們能夠將輪詢階段分紅幾個不一樣的部分。
輪詢的下一個階段是 check pahse,這個專用於 setImmediate 的階段。爲何須要一個專門的隊列來處理 setImmediate 回調?這是由於輪詢階段的行爲,待會兒將在流程部分討論。如今只須要記住檢查(check)階段主要處理 setImmediate() 的回調。
回調的關閉(stocket.on('close', () => {}))都在這裏處理的,更像一個清理階段。
nextTickQueue 中的任務保留在被 process.nextTick() 觸發的回調。 microTaskQueue 保留着被 Promise 觸發的回調。它們都不是事件循環地一部分(不是在 libUV 中開發地),而是在 node 中。在 C/C++ 和 Javascript 有交叉的時候,它們都是儘量快地被調用。所以它們應該在當前操做運行後(不必定是當前 js 回調執行完)。
當在你的控制檯運行 node my-script.js ,node 設置事件循環而後運行你主要的模塊(my-script.js)事件循環的外部。一旦主要模塊執行完,node 將會檢查循環是否還活着(事件循環中是否還有事情要作)?若是沒有,將會在執行退出回調後退出。process, on('exit', foo) 回調(退出回調)。可是若是循環還活着,node 將會從計時器階段進入循環。
事件循環進入計時器階段而且檢查在計時器隊列中是否有須要執行的。好吧,這句話聽起來很是簡單,可是事件循環實際上要執行一些步驟發現合適的回調。實際上計時器腳本以升序儲存在堆內存中。它首先獲取到一個執行計時器,計算下是否 now-registeredTime == delta?若是是,他會執行這個計時器的回調而且檢查下一個計時器。直到找到一個尚未約定時間的計時器,它會中止檢查其餘的定時器(由於定時器都以升序排好了)而且移到下一個階段了。
假設你調用了 setTimeout 4次建立了4個定時器,分別相對於時間 t 來講 100,200,300,400 的差值。
假設事件循環在 t+250 進入到了計時器階段。它會首先看下計時器 A,A 的過時時間是 t+100。可是如今時間是 t+250。所以它將執行綁定在計時器 A 上的回調。而後去檢查計時器 B,發現它的過時時間是 t+200,所以也會執行 B 的回調。如今它會檢查 C,發現它的過時時間是 t+300,所以將會離開它。時間循環不會去檢查 D,由於計時器是按升序拍好的;所以 D 的閾值比 C 大。然而這個階段有一個系統相關的硬限制,若是達到系統依賴最大限制數量,即便有未執行的計時器,它也會移到下一個階段。
計時器階段後,事件循環將會進入到了懸而未決的 I/O 階段,而後檢查一下 pengding_queue 中是否有來自於以前的懸而未決的任務的回調。若是有,一個接一個的執行,直到隊列爲空,或者達到系統的最大限制。以後,事件循環將會移到 idle handler 階段,其次是準備階段作一些內部的操做。而後最終可能進入到最重要的階段 poll phase。
像名字說的那樣,這是一個觀察的階段。觀察是否有新的請求或者鏈接傳入。當事件循環進入輪詢階段,它會在 watcher_queue 中執行腳本,包含文件讀響應,新的 socket 或者 http 鏈接請求,直到事件耗盡或者像其餘階段那樣達到系統依賴上限。假設沒有要執行的回調,輪詢在某些特定的條件下將會等待一下子。若是在檢查隊列(check queue),懸而未決隊列(pending queue),或者關閉隊列(closing callbacks queue 或者 idle handler queue)裏面有任何任務等待,它將等待 0 毫秒。而後它會根據定時器堆來決定等待時間執行第一個定時器(若是可獲取)。若是第一個定時器閾值通過了,毫無疑問它不須要等待(就會執行第一個定時器)。
輪詢階段結束以後,當即來到檢查階段。這個階段的隊列中有被 api setImmediate 觸發的回調。它將會像其餘階段那樣一個接着一個的執行,直到隊列爲空或者達到依賴系統的最大限制。
完成在檢查階段的任務以後,事件循環的下一個目的地是處理關閉或者銷燬類型的回調 close callback。事件循環執行完這個階段的隊列中的回調後,它會檢查循環(loop)是否還活着,若是沒有,退出。可是若是還有工做要作,它會進入下一個循環;所以在計時器階段。若是你認爲以前例子中的定時器(A & B)過時,那麼如今定時器階段將會從定時器 C 開始檢查是否過時。
所以,這兩個隊列的回調函數何時運行?它們固然在從當前階段到下一個階段以前儘量快的運行。不像其餘階段,它們兩個沒有系統依賴的醉倒限制,node 運行它們直到兩個隊列是空的。然而,nextTickQueue 會比 microTaskQueue 有着更高的任務優先級。
我從 Javascript 開發者哪裏聽到廣泛的一個詞就是 ThreadPool。一個廣泛的誤解是,nodejs 有一個處理全部異步操做的進程池。可是實際上進程池是 libUV (nodejs用來處理異步的第三方庫)庫中的。之因此沒有在圖中畫出來,是由於它不是循環機制的一部分。目前,並非每一個異步任務都會被進程池處理的。libUV 可以靈活使用操做系統的異步 api 來保持環境爲事件驅動。然而操做系統的 api 不能作文件讀取,dns 查詢等,這些由進程池來處理,默認只有 4 個進程。你能夠經過設置 uv_threadpool_size 的環境變量增長進程數直到 128.
但願你能理解事件循環是如何工做的。C 語言 中同步的 while 幫助 Javascript 成爲異步的。每次只處理一件事可是很吶阻塞。固然,不管咱們若是描述理論,最好的理解仍是示例,所以,讓咱們經過一些代碼片斷來理解這個腳本。
setTimeout(() => {console.log('setTimeout'); }, 0); setImmediate(() => {console.log('setImmediate'); });
你可以猜到上面的輸出嗎?好吧,你可能認爲 setTimeout 會先被打印出來,可是不能保證,爲何呢?執行完主模塊以後進入計時器階段,他可能不會或者會發現你的計時器耗盡了。爲何呢?一個計時器腳本是根據系統時間和你提供的增量時間註冊的。setTimeout 調用的同時,計時器腳本被寫入到了內存中,根據你的機器性能和其餘運行在它上面的操做(不是node)的不一樣,可能會有一個很小的延遲。另外一點時,node僅僅在進入計時器階段(每一輪遍歷)以前設置一個變量 now,將 now 做爲當前時間。所以你能夠說至關於精確的時間有點問題。這就是不肯定性的緣由。若是你在一個計時器代碼的回調裏面指向相同的代碼會獲得相同的結果。
然而,若是你移動這段代碼到 i/o 週期裏,保證 setImmediate 回調會先於 setTimeout 運行。
fs.readFile('my-file-path.txt', () => { setTimeout(() => {console.log('setTimeout');}, 0); setImmediate(() => {console.log('setImmediate');}); });
var i = 0; var start = new Date(); function foo () { i++; if (i < 1000) { setImmediate(foo); } else { var end = new Date(); console.log("Execution time: ", (end - start)); } } foo();
上面的例子很是簡單。調用函數 foo 函數內部再經過 setImmediate 遞歸調用 foo 直到 1000。在個人電腦上面,大概花費了 6 到 8 毫秒。仙子啊修改下上面的代碼,把 setImmedaite(foo) 換成 setTimeout(foo, o)。
var i = 0; var start = new Date(); function foo () { i++; if (i < 1000) { setTimeout(foo, 0); } else { var end = new Date(); console.log("Execution time: ", (end - start)); } } foo();
如今在個人電腦上面運行這段代碼花費了 1400+ms。爲何會這樣?它們都沒有 i/o 事件,應該同樣纔對。上面兩個例子等待事件是 0.爲何花費這麼長時間?經過事件比較找到了誤差,CPU 密集型任務,花費更多的時間。註冊計時器腳本也花費事件。定時器的每一個階段都須要作一些操做來決定一個定時器是否應該執行。長時間的執行也會致使更多的 ticks。然而,在 setImmediate 中,只有檢查這一個階段,就好像在一個隊列裏面而後執行就好了。
var i = 0; function foo(){ i++; if (i>20) return; console.log("foo"); setTimeout(()=>console.log("setTimeout"), 0); process.nextTick(foo); } setTimeout(foo, 2000);
你認爲上面輸出是什麼?是的,它會輸出 foo 而後輸出 setTimeout。2秒後被 nextTickQueue 遞歸調用 foo() 打印出第一個 foo。當全部的 nextTickQueue 執行完了,開始執行其餘(好比 setTimeout 回調)的。
因此是每一個回調執行完以後,開始檢查 nextTickQueue 的嗎? 咱們改下代碼看下。
var i = 0; function foo(){ i++; if (i>20) return; console.log("foo"); setTimeout(()=>console.log("setTimeout"), 0); process.nextTick(foo); } setTimeout(foo, 2000); setTimeout(()=>{console.log("Other setTimeout"); }, 2000);
在 setTimeout 以後,我僅僅用同樣的延遲時間添加了另外一個輸出 Other setTimeout 的 setTimeout。儘管不能保證,可是有可能會在輸出第一個 foo 以後輸出 Other setTimeout 。相同的定時器分爲一個組,nextTickQueue 會在正在進行中的回調組執行完以後執行。
就像咱們大多數人都認爲事件循環是在一個單獨的線程裏面,將回調推入一個隊列,而後一個接着一個執行。第一次讀到這篇文章的讀者可能會感到疑惑,Javascript 在哪裏執行的?正如我早些時候說的,只有一個線程,來自於自己使用 V8 或者其餘引擎的事件循環的 Javascript 代碼也是在這裏運行的。執行是同步的,若是當前的 Javascript 執行尚未完成,事件循環不會傳播。
首先不是0,而是1.當你設置一個計時器,時間爲小於 1,或者大於 2147483647ms 的時候,它會自動設置爲 1.所以你若是設置 setTimeout 的延遲時間爲 0,它會自動設置爲1.
此外,setImmediate 會減小額外的檢查。所以 setImmediate 會執行的更快一些。它也放置在輪詢階段以後,所以來自於任何一個到來的請求 setImmediate 回調將會當即被執行。
setImmediate 和 process.nextTick() 都命名錯了。因此功能上,setImmediate 在下一個 tick 執行,nextTick 是立刻執行的。
因爲 nextTickQueue 沒有回調執行的限制。所以若是你遞歸地執行 process.nextTick(),你地程序可能永遠在事件循環中出不來,不管你在其餘階段有什麼。
它可能會初始化計時器,但回調可能永遠不會被調用。由於若是 node 在 exit callback 階段,它已經跳出事件循環了。所以沒有回去執行。
事件循環沒有工做棧
事件循環不在一個單獨地線程裏面,Javascript 的執行也不是像從隊列中彈出一個回調執行那麼簡單。
setImmediate 沒有將回調推入到工做隊列地頭部,有一個專門的階段和隊列。
setImmediate 在下一個循環執行,nextTick 其實是立刻執行。
小心,若是遞歸調用的話,nextTickQueue 可能會阻塞你的 node 代碼。