這篇文章帶給大家的內容是關於Node.js的事件循環工作流程以及生命週期的詳細講解,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。
本文,將會詳細的解說node.js 事件循環工作流程與生命週期
圖圖中不同的盒子代表不同的階段,每個階段執行特定的工作。每個階段都有一個佇列(這裡說成佇列主要是為了更好理解;真實的資料結構可能不是佇列),Javascript 可以在任何一個階段執行(除了 idle & prepare)。你在圖片中也能看到 nextTickQueue 和 microTaskQueue,它們不是迴圈的一部分,它們之中的回呼可以在任意階段執行。它們有更高的優先權去執行。 現在你知道了事件循環是不同階段和不同隊列的結合;以下是每個階段的描述。
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 <p>上面的範例非常簡單。呼叫函數 foo 函數內部再透過 setImmediate 遞歸呼叫 foo 直到 1000。在我的電腦上面,大概花了 6 到 8 毫秒。仙子啊修改下上面的程式碼,把 setImmedaite(foo) 換成 setTimeout(foo, o)。 </p><pre class="brush:php;toolbar:false">var i = 0; var start = new Date(); function foo () { i++; if (i <p>現在在我的電腦上面運行這段程式碼花費了 1400 ms。為什麼會這樣呢?它們都沒有 i/o 事件,應該一樣才對。上面兩個例子等待事件是 0.為什麼要花這麼久?透過事件比較找到了偏差,CPU 密集型任務,花費更多的時間。註冊計時器腳本也花費事件。定時器的每個階段都需要做一些操作來決定一個定時器是否應該執行。長時間的執行也會導致更多的 ticks。然而,在 setImmediate 中,只有檢查這一個階段,就好像在一個佇列裡面然後執行就行了。 </p><h5><strong>片段3 — 理解 nextTick() & 計時器(timer)執行</strong></h5><pre class="brush:php;toolbar:false">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 程式碼。
相關推薦:
#以上是Node.js的事件循環工作流程以及生命週期的詳細解說的詳細內容。更多資訊請關注PHP中文網其他相關文章!