摘要:定時器階段這個是事件循環開始的階段,綁定到這個階段的隊列,保留著定時器的回調,盡管它并沒有將回調推入隊列中,但是以最小的堆來維持計時器并且在到達規定的事件后執行回調。
本文,將會詳細的講解 node.js 事件循環工作流程和生命周期
一些常見的誤解最常見的誤解之一,事件循環是 Javascript 引擎(V8,spiderMonkey等)的一部分。事實上事件循環主要利用 Javascript 引擎來執行代碼。
首先沒有棧,其次這個過程是復雜的,有多個隊列(像數據結構中的隊列)參與。但是大多數開發者知道多少有的回調函數被推進一個單一的隊列里面,是完全錯誤的。
由于錯誤的 node.js 事件循環圖,我們中有一部分人認為u有兩個線程。一個執行 Javascript,另一個執行事件循環。事實上都在一個線程里面運行。
另一個非常大的誤解是 setTimeout 的回調函數在給定的延遲完成之后被(可能是 OS 或者 內核)推進一個隊列。
作為常見的事件循環描述只有一個隊列;所以一些開發者認為 setImmediate 將回調放在工作隊列的前面。這是完全錯誤的,在 Javascript 的工作隊列都是先進先出的。
事件循環的架構在我們開始描述事件循環的工作流程時,知道它的架構非常重要。下圖為事件循環真正的工作流程:
圖中不同的盒子代表不同的階段,每個階段執行特定的工作。每個階段都有一個隊列(這里說成隊列主要是為了更好理解;真實的數據結構可能不是隊列),Javascript 可以在任何一個階段執行(除了 idle & prepare)。你在圖片中也能看到 nextTickQueue 和 microTaskQueue,它們不是循環的一部分,它們之中的回調可以在任意階段執行。它們有更高的優先級去執行。
現在你知道了事件循環是不同階段和不同隊列的結合;下面是每個階段的描述。
這個是事件循環開始的階段,綁定到這個階段的隊列,保留著定時器(setTimeout, setInterval)的回調,盡管它并沒有將回調推入隊列中,但是以最小的堆來維持計時器并且在到達規定的事件后執行回調。
這個階段執行在事件循環中 pending_queue 里的回調,這些回調時被之前的操作推入的。例如當你嘗試往 tcp 中寫入一些東西,這個工作完成了,然后回調被推入到隊列中。錯誤處理的回調也在這里。
盡管名字是空閑(idle),但是每個 tick 都運行。Prepare 也在輪詢階段開始之前運行。不管怎樣,這兩個階段是 node 主要做一些內部操作的階段。
可能整個事件循環最重要的一個階段就是 poll phase。這個階段接受新傳入的連接(新的 Socket 建立等)和數據(文件讀取等)。我們可以將輪詢階段分成幾個不同的部分。
如果在 watch_queue (這個隊列被綁定到輪詢階段)有東西,它們將會被一個接著一個的執行知道隊列為空或者系統達到最大的限制。
一旦隊列為空,node 就會等待新的連接。等待或者睡眠的事件取決于多種因素。
輪詢的下一個階段是 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 毫秒。然后它會根據定時器堆來決定等待時間執行第一個定時器(如果可獲?。H绻谝粋€定時器閾值經過了,毫無疑問它不需要等待(就會執行第一個定時器)。
輪詢階段結束之后,立即來到檢查階段。這個階段的隊列中有被 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 代碼。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/108190.html
摘要:輪詢投票處理下一次處理的新事件立即設置運行通過注冊的所有回調關閉執行所有的回調工作處理延遲此度量標準測量線程池處理異步任務需要多長時間。高工作時間處理延遲表明繁忙耗盡的線程池。 原文=> What you should know to really understand the Node.js Event Loop Node.js 是一個基于事件的平臺。這就意味著在Node中發生的所...
摘要:變量的說法來自于,這是在多線程模型下出現并發問題的一種解決方案。目前已經有庫實現了應用層棧幀的可控編碼,同時可以在該棧幀存活階段綁定相關數據,我們便可以利用這種特性實現類似多線程下的變量。 ThreadLocal變量的說法來自于Java,這是在多線程模型下出現并發問題的一種解決方案。ThreadLocal變量作為線程內的局部變量,在多線程下可以保持獨立,它存在于線程的生命周期內,可以在...
摘要:前端每周清單第期現狀分析與優化策略單元測試爬蟲作者王下邀月熊編輯徐川前端每周清單專注前端領域內容,以對外文資料的搜集為主,幫助開發者了解一周前端熱點分為新聞熱點開發教程工程實踐深度閱讀開源項目巔峰人生等欄目。 showImg(https://segmentfault.com/img/remote/1460000011008022); 前端每周清單第 29 期:Web 現狀分析與優化策略...
摘要:需要校驗字節信息是否符合規范,避免惡意信息和不規范數據危害運行安全。具有相同哈希值的鍵值對會組成鏈表。通過在協議下添加了一層協議對數據進行加密從而保證了安全。常見的非對稱加密包括等。 類加載過程 Java 中類加載分為 3 個步驟:加載、鏈接、初始化。 加載。 加載是將字節碼數據從不同的數據源讀取到JVM內存,并映射為 JVM 認可的數據結構,也就是 Class 對象的過程。數據源可...
閱讀 3712·2023-04-25 17:45
閱讀 3426·2021-09-04 16:40
閱讀 999·2019-08-30 13:54
閱讀 2126·2019-08-29 12:59
閱讀 1396·2019-08-26 12:11
閱讀 3273·2019-08-23 15:17
閱讀 1516·2019-08-23 12:07
閱讀 3878·2019-08-22 18:00