首頁  >  文章  >  web前端  >  Node.js的事件循環工作流程以及生命週期的詳細解說

Node.js的事件循環工作流程以及生命週期的詳細解說

不言
不言原創
2018-08-15 14:26:472283瀏覽

這篇文章帶給大家的內容是關於Node.js的事件循環工作流程以及生命週期的詳細講解,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。

本文,將會詳細的解說node.js 事件循環工作流程與生命週期

一些常見的誤解

##在js引擎內部的事件循環
最常見的誤解之一,事件循環是Javascript 引擎(V8,spiderMonkey等)的一部分。事實上事件循環主要利用 Javascript 引擎來執行程式碼。

有一個堆疊或佇列
首先沒有棧,其次這個過程是複雜的,有多個佇列(像資料結構中的佇列)參與。但是大多數開發者知道多少有的回呼函數被推進一個單一的佇列裡面,是完全錯誤的。

事件循環運行在一個單獨的執行緒裡面
由於錯誤的 node.js 事件循環圖,我們有一部分人認為u有兩個執行緒。一個執行 Javascript,另一個執行事件循環。事實上都在一個執行緒裡面運行。

在setTimeout 中有非同步的OS 的系統參與
#另一個非常大的誤解是setTimeout 的回呼函數在給定的延遲完成之後被(可能是OS 或者核心)推進一個佇列。

setImmediate 將回呼函數放在第一個位置
#作為常見的事件循環描述只有一個佇列;所以有些開發者認為setImmediate 將回呼放在工作佇列的前面。這是完全錯誤的,在Javascript 的工作佇列都是先進先出的

事件循環的架構

在我們開始描述事件循環的工作流程時,知道它的架構非常重要。下圖為事件循環真正的工作流程:

Node.js的事件循環工作流程以及生命週期的詳細解說

圖圖中不同的盒子代表不同的階段,每個階段執行特定的工作。每個階段都有一個佇列(這裡說成佇列主要是為了更好理解;真實的資料結構可能不是佇列),Javascript 可以在任何一個階段執行(除了 idle & prepare)。你在圖片中也能看到 nextTickQueue 和 microTaskQueue,它們不是迴圈的一部分,它們之中的回呼可以在任意階段執行。它們有更高的優先權去執行。

現在你知道了事件循環是不同階段和不同隊列的結合;以下是每個階段的描述。

計時器(Timer)階段
這個是事件循環開始的階段,綁定到這個階段的佇列,保留計時器(setTimeout, setInterval)的回調,儘管它並沒有將回調推入佇列中,但是以最小的堆來維持計時器並且在到達規定的事件後執行回調。

懸而未決的(Pending)I/O 回呼階段
這個階段執行在事件循環中pending_queue 裡的回調,這些回呼時被之前的操作推入的。例如當你嘗試在 tcp 中寫入一些東西,這個工作完成了,然後回調被推入到佇列中。錯誤處理的回調也在這裡。

Idle, Prepare 階段
儘管名字是空閒(idle),但是每個 tick 都運行。 Prepare 也在輪詢階段開始之前運作。不管怎樣,這兩個階段是 node 主要做一些內部操作的階段。

輪詢(Poll)階段
可能整個事件循環最重要的一個階段就是 poll phase。這個階段接受新傳入的連線(新的 Socket 建立等)和資料(檔案讀取等)。我們可以將輪詢階段分成幾個不同的部分。

  1. 如果在 watch_queue (這個隊列被綁定到輪詢階段)有東西,它們將會被一個接著一個的執行知道隊列為空或系統達到最大的限制。

  2. 一旦佇列為空,node 就會等待新的連線。等待或睡眠的事件取決於多種因素。

檢查(Check)階段
下一個階段的輪詢是 check pahse,這個專用於 setImmediate 的階段。為什麼需要一個專門的佇列來處理 setImmediate 回呼?這是因為輪詢階段的行為,待會兒將在流程部分討論。現在只需要記住檢查(check)階段主要處理 setImmediate() 的回調。

關閉(Close)回呼
#回呼的關閉(stocket.on('close', () => {}))都在這裡處理的,更像是清理階段。

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 將會從計時器階段進入循環。

Node.js的事件循環工作流程以及生命週期的詳細解說

#計時器階段(Timer phase)的工作流程

事件循環進入計時器階段並且檢查在計時器佇列中是否有需要執行的。好吧,這句話聽起來非常簡單,但是事件循環實際上要執行一些步驟來發現合適的回調。實際上計時器腳本以升序儲存在堆記憶體中。它首先取得到一個執行計時器,計算下是否 now-registeredTime == delta?如果是,他會執行這個計時器的回調並且檢查下一個計時器。直到找到一個還沒有約定時間的計時器,它會停止檢查其他的定時器(因為定時器都以升序排好了)並且移到下一個階段了。
假設你呼叫了 setTimeout 4次創建了4個定時器,分別相對於時間 t 來說 100,200,300,400 的差值。

Node.js的事件循環工作流程以及生命週期的詳細解說

假設事件循環在 t 250 進入到了計時器階段。它會先看下計時器 A,A 的過期時間是 t 100。但是現在時間是 t 250。因此它將執行綁定在計時器 A 上的回呼。然後去檢查計時器 B,發現它的過期時間是 t 200,因此也會執行 B 的回呼。現在它會檢查 C,發現它的過期時間是 t 300,因此將會離開它。時間循環不會去檢查 D,因為計時器是按升序拍好的;因此 D 的閾值比 C 大。然而這個階段有一個系統相關的硬限制,如果達到系統依賴最大限制數量,即使有未執行的計時器,它也會移到下一個階段。

懸而未決(Pengding phase)的I/O 階段工作流程

計時器階段後,事件循環將會進入到了懸而未決的I/O 階段,然後檢查一下pengding_queue 中是否有來自於先前的懸而未決的任務的回調。如果有,一個接一個的執行,直到隊列為空,或達到系統的最大限制。之後,事件循環將會移到 idle handler 階段,其次是準備階段做一些內部的操作。然後最終可能進入到最重要的階段 poll phase。

輪詢階段(Poll phase)工作流程

就像名字說的那樣,這是一個觀察的階段。觀察是否有新的請求或連線傳入。當事件循環進入輪詢階段,它會在 watcher_queue 中執行腳本,包含檔案讀取回應,新的 socket 或 http 連線請求,直到事件耗盡或像其他階段一樣達到系統依賴上限。假設沒有要執行的回調,輪詢在某些特定的條件下將會等待一會兒。如果在檢查佇列(check queue),懸而未決佇列(pending queue),或關閉佇列(closing callbacks queue 或 idle handler queue)裡面有任何任務等待,它將等待 0 毫秒。然後它會根據定時器堆來決定等待時間執行第一個定時器(如果可取得)。如果第一個定時器閾值經過了,毫無疑問它不需要等待(就會執行第一個定時器)。

檢查階段(Check phase)工作流程

輪詢階段結束之後,立即來到檢查階段。這個階段的佇列中有被 api setImmediate 觸發的回呼。它將會像其他階段一樣一個接著一個的執行,直到隊列為空或達到依賴系統的最大限制。

關閉回呼(Close callback)的工作流程

完成在檢查階段的任務之後,事件循環的下一個目的地是處理關閉或銷毀類型的回呼 close callback。事件循環執行完這個階段的佇列中的回調後,它會檢查循環(loop)是否還活著,如果沒有,退出。但是如果還有工作要做,它會進入下一個循環;因此在計時器階段。如果你認為先前範例中的定時器(A & B)過期,那麼現在定時器階段將會從定時器 C 開始檢查是否過期。

nextTickQueue & microTaskQueue

因此,這兩個佇列的回呼函數什麼時候會運行?它們當然在從當前階段到下一個階段之前盡可能快的運行。不像其他階段,它們兩個沒有系統依賴的醉倒限制,node 運行它們直到兩個隊列是空的。然而,nextTickQueue 會比  microTaskQueue 有更高的任務優先順序。

進程池(Thread-pool)

我從 Javascript 開發者哪裡聽到普遍的一個字就是 ThreadPool。一個普遍的誤解是,nodejs 有一個處理所有非同步操作的進程池。但實際上進程池是 libUV (nodejs用來處理非同步的第三方函式庫)庫中的。之所以沒有在圖中畫出來,是因為它不是循環機制的一部分。目前,並不是每個非同步任務都會被進程池處理的。 libUV 能夠靈活地使用作業系統的非同步 api 來保持環境為事件驅動。然而作業系統的 api 不能做檔案讀取,dns 查詢等,這些由進程池來處理,預設只有 4 個進程。你可以透過設定 uv_threadpool_size 的環境變數增加進程數直到 128.

帶有範例的工作流程

希望你能理解事件循環是如何運作的。 C 語言 中同步的 while 幫助 Javascript 成為非同步的。每次只處理一件事但是很吶阻塞。當然,無論我們如果描述理論,最好的理解還是示例,因此,讓我們透過一些程式碼片段來理解這個腳本。

片段1—基礎理解
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');}); });
片段2 — 更好的理解計時器
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 程式碼在哪裡執行的?

就像我們大多數人都認為事件循環是在一個單獨的執行緒裡面,將回呼推入一個佇列,然後一個接著一個執行。第一次讀到這篇文章的讀者可能會感到疑惑,Javascript 在哪裡執行的?正如我早些時候所說的,只有一個線程,來自於本身使用 V8 或其他引擎的事件循環的 Javascript 程式碼也是在這裡運行的。執行是同步的,如果目前的 Javascript 執行還沒有完成,事件循環不會傳播。

我們有了 setTimeout(fn, 0),為什麼還需要 setImmediate?

首先不是0,而是1.當你設定計時器,時間為小於1,或大於2147483647ms 的時候,它會自動設定為1.因此你如果設定setTimeout 的延遲時間為0,它會自動設定為1.

此外,setImmediate 會減少額外的檢查。因此 setImmediate 會執行的更快一些。它也被放置在輪詢階段之後,因此來自於任何一個到來的請求 setImmediate 回呼將會立即被執行。

為什麼 setImmediate 會被理解呼叫?

setImmediate 和 process.nextTick() 都命名錯了。所以功能上,setImmediate 在下一個 tick 執行,nextTick 是馬上執行的。

Javascript 程式碼會被封鎖嗎?

由於 nextTickQueue 沒有回呼執行的限制。因此如果你遞歸地執行 process.nextTick(),你地程式可能永遠在事件循環中出不來,無論你在其他階段有什麼。

如果我在 exit callback 階段呼叫 setTimeout 會怎麼樣?

它可能會初始化計時器,但回呼可能永遠不會被呼叫。因為如果 node 在 exit callback 階段,它已經跳出事件循環了。因此沒有回去執行。

一些短結論

事件循環沒有工作堆疊

事件循環不在一個單獨地執行緒裡面,Javascript 的執行也不像從佇列中彈出一個回調執行那麼簡單。

setImmediate 沒有將回呼推入到工作佇列地頭部,有一個專門的階段和佇列。

setImmediate 在下一個迴圈執行,nextTick 其實是馬上執行。

當心,如果遞歸呼叫的話,nextTickQueue 可能會阻塞你的 node 程式碼。

相關推薦:

深入淺析Node.js 事件循環_node.js

JS控制項的生命週期介紹_javascript技巧

#

以上是Node.js的事件循環工作流程以及生命週期的詳細解說的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn