摘要:開始執行文件,同步代碼執行完畢后,進入事件循環。時間未到的時候,如果有事件返回,就執行該事件注冊的回調函數。對于多次執行輸出結果不同,需要了解事件循環的基礎問題。
1. 說明
nodejs是單線程執行的,同時它又是基于事件驅動的非阻塞IO編程模型。這就使得我們不用等待異步操作結果返回,就可以繼續往下執行代碼。當異步事件觸發之后,就會通知主線程,主線程執行相應事件的回調。
本篇文章講解node中JavaScript的代碼的執行流程,下面是測試代碼,如果你知道輸出的結果,那么就不需要再看本篇文章,如果不知道輸出結果,那么本片文章可幫助你了解:
console.log(1) setTimeout(function () { new Promise(function (resolve) { console.log(2) resolve() }) .then(() => { console.log(3) }) }) setTimeout(function () { console.log(4) })
復雜的:
setTimeout(() => { console.log("1") new Promise((resolve) => { console.log("2"); resolve(); }) .then(() => { console.log("3") }) new Promise((resolve)=> { console.log("4"); resolve()}) .then(() => { console.log("5") }) setTimeout(() => { console.log("6") setTimeout(() => { console.log("7") new Promise((resolve) => { console.log("8"); resolve() }) .then( () => { console.log("9") }) new Promise((resolve) => { console.log("10"); resolve() }) .then(() => { console.log("11") }) }) setTimeout(() => { console.log("12") }, 0) }) setTimeout(() => { console.log("13") }, 0) }) setTimeout(() => { console.log("14") }, 0) new Promise((resolve) => { console.log("15"); resolve() }) .then( ()=> { console.log("16") }) new Promise((resolve) => { console.log("17"); resolve() }) .then(() => { console.log("18") })2. nodejs的啟動過程
node.js啟動過程可以分為以下步驟:
調用platformInit方法 ,初始化 nodejs 的運行環境。
調用 performance_node_start 方法,對 nodejs 進行性能統計。
openssl設置的判斷。
調用v8_platform.Initialize,初始化 libuv 線程池。
調用 V8::Initialize,初始化 V8 環境。
創建一個nodejs運行實例。
啟動上一步創建好的實例。
開始執行js文件,同步代碼執行完畢后,進入事件循環。
在沒有任何可監聽的事件時,銷毀 nodejs 實例,程序執行完畢。
3. nodejs的事件循環詳解Nodejs 將消息循環又細分為 6 個階段(官方叫做 Phase), 每個階段都會有一個類似于隊列的結構, 存儲著該階段需要處理的回調函數.
Nodejs 為了防止某個 階段 任務太多, 導致后續的 階段 發生饑餓的現象, 所以消息循環的每一個迭代(iterate) 中, 每個 階段 執行回調都有個最大數量. 如果超過數量的話也會強行結束當前 階段而進入下一個 階段. 這一條規則適用于消息循環中的每一個 階段.
3.1 Timer 階段這是消息循環的第一個階段, 用一個 for 循環處理所有 setTimeout 和 setInterval 的回調.
這些回調被保存在一個最小堆(min heap) 中. 這樣引擎只需要每次判斷頭元素, 如果符合條件就拿出來執行, 直到遇到一個不符合條件或者隊列空了, 才結束 Timer Phase.
Timer 階段中判斷某個回調是否符合條件的方法也很簡單. 消息循環每次進入 Timer 的時候都會保存一下當時的系統時間,然后只要看上述最小堆中的回調函數設置的啟動時間是否超過進入 Timer 時保存的時間, 如果超過就拿出來執行.
3.2 Pending I/O Callback 階段執行除了close callbacks、setTimeout()、setInterval()、setImmediate()回調之外幾乎所有回調,比如說TCP連接發生錯誤、 fs.read, socket 等 IO 操作的回調函數, 同時也包括各種 error 的回調.
3.3 Idle, Prepare 階段系統內部的一些調用。
3.4 Poll 階段,重要階段這是整個消息循環中最重要的一個 階段, 作用是等待異步請求和數據,因為它支撐了整個消息循環機制.
poll階段有兩個主要的功能:一是執行下限時間已經達到的timers的回調,一是處理poll隊列里的事件。
注:Node的很多API都是基于事件訂閱完成的,比如fs.readFile,這些回調應該都在poll階段完成。
當事件循環進入poll階段:
poll隊列不為空的時候,事件循環肯定是先遍歷隊列并同步執行回調,直到隊列清空或執行回調數達到系統上限。
poll隊列為空的時候,這里有兩種情況。
如果代碼已經被setImmediate()設定了回調,那么事件循環直接結束poll階段進入check階段來執行check隊列里的回調。
如果代碼沒有被設定setImmediate()設定回調:
如果有被設定的timers,那么此時事件循環會檢查timers,如果有一個或多個timers下限時間已經到達,那么事件循環將繞回timers階段,并執行timers的有效回調隊列。
如果沒有被設定timers,這個時候事件循環是阻塞在poll階段等待事件回調被加入poll隊列。
Poll階段,當js層代碼注冊的事件回調都沒有返回的時候,事件循環會暫時阻塞在poll階段,解除阻塞的條件:
3.5 Check 階段在poll階段執行的時候,會傳入一個timeout超時時間,該超時時間就是poll階段的最大阻塞時間。
timeout時間未到的時候,如果有事件返回,就執行該事件注冊的回調函數。timeout超時時間到了,則退出poll階段,執行下一個階段。
這個 timeout 設置為多少合適呢? 答案就是 Timer Phase 中最近要執行的回調啟動時間到現在的差值, 假設這個差值是 detal. 因為 Poll Phase 后面沒有等待執行的回調了. 所以這里最多等待 delta 時長, 如果期間有事件喚醒了消息循環, 那么就繼續下一個 Phase 的工作; 如果期間什么都沒發生, 那么到了 timeout 后, 消息循環依然要進入后面的 Phase, 讓下一個迭代的 Timer Phase 也能夠得到執行.
Nodejs 就是通過 Poll Phase, 對 IO 事件的等待和內核異步事件的到達來驅動整個消息循環的.
這個階段只處理 setImmediate 的回調函數.
那么為什么這里要有專門一個處理 setImmediate 的 階段 呢? 簡單來說, 是因為 Poll 階段可能設置一些回調, 希望在 Poll 階段 后運行. 所以在 Poll 階段 后面增加了這個 Check 階段.
專門處理一些 close 類型的回調. 比如 socket.on("close", ...). 用于資源清理.
4. nodejs執行JS代碼過程及事件循環過程
1、node初始化
初始化node環境
執行輸入的代碼
執行process.nextTick回調
執行微任務(microtasks)
2、進入事件循環
2.1、進入Timer階段
檢查Timer隊列是否有到期的Timer的回調,如果有,將到期的所有Timer回調按照TimerId升序執行
檢查是否有process.nextTick任務,如果有,全部執行
檢查是否有微任務(promise),如果有,全部執行
退出該階段
2.2、進入Pending I/O Callback階段
檢查是否有Pending I/O Callback的回調,如果有,執行回調。如果沒有退出該階段
檢查是否有process.nextTick任務,如果有,全部執行
檢查是否有微任務(promise),如果有,全部執行
退出該階段
2.3、進入idle,prepare階段
這個階段與JavaScript關系不大,略過
2.4、進入Poll階段
首先檢查是否存在尚未完成的回調,如果存在,分如下兩種情況:
第一種情況:有可執行的回調
執行所有可用回調(包含到期的定時器還有一些IO事件等)
檢查是否有process.nextTick任務,如果有,全部執行
檢查是否有微任務(promise),如果有,全部執行
退出該階段
第二種情況:沒有可執行的回調
檢查是否有immediate回調,如果有,退出Poll階段。如果沒有,阻塞在此階段,等待新的事件通知
如果不存在尚未完成的回調,退出Poll階段
2.5、進入check階段
如果有immediate回調,則執行所有immediate回調
檢查是否有process.nextTick任務,如果有,全部執行
檢查是否有微任務(promise),如果有,全部執行
退出該階段
2.6、進入closing階段
如果有immediate回調,則執行所有immediate回調
檢查是否有process.nextTick任務,如果有,全部執行
檢查是否有微任務(promise),如果有,全部執行
退出該階段
3、檢查是否有活躍的handles(定時器、IO等事件句柄)
如果有,繼續下一輪事件循環
如果沒有,結束事件循環,退出程序
注意:
事件循環的每一個子階段退出之前都會按順序執行如下過程:
檢查是否有 process.nextTick 回調,如果有,全部執行。
檢查是否有 微任務(promise),如果有,全部執行。
4.1 關于Promise和process.nextTick事件循環隊列先保證所有的process.nextTick回調,然后將所有的Promise回調追加在后面,最終在每個階段結束的時候一次性拿出來執行。
此外,process.nextTick和Promise回調的數量是受限制的,也就是說,如果一直往這個隊列中加入回調,那么整個事件循環就會被卡住。
4.2 關于setTimeout(…, 0) 和 setImmediate這兩個方法的回調到底誰快?
如下面的例子:
setImmediate(() => console.log(2)) setTimeout(() => console.log(1))
使用nodejs多次執行后,發現輸出結果有時是1 2,有時是2 1。
對于多次執行輸出結果不同,需要了解事件循環的基礎問題。
首先,Nodejs啟動,初始化環境后加載我們的JS代碼(index.js).發生了兩件事(此時尚未進入消息循環環節):
setImmediate 向 Check 階段 中添加了回調 console.log(2);setTimeout 向 Timer 階段 中添加了回調 console.log(1)
這時候, 要初始化階段完畢, 要進入 Nodejs 消息循環了。
為什么會有兩種輸出呢? 接下來一步很關鍵:
當執行到 Timer 階段 時, 會發生兩種可能. 因為每一輪迭代剛剛進入 Timer 階段 時會取系統時間保存起來, 以 ms(毫秒) 為最小單位.
如果 Timer 階段 中回調預設的時間 > 消息循環所保存的時間, 則執行 Timer 階段 中的該回調. 這種情況下先輸出 1, 直到 Check 階段 執行后,輸出2.總的來說, 結果是 1 2.
如果運行比較快, Timer 階段 中回調預設的時間可能剛好等于消息循環所保存的時間, 這種情況下, Timer 階段 中的回調得不到執行, 則繼續下一個 階段. 直到 Check 階段, 輸出 2. 然后等下一輪迭代的 Timer 階段, 這時的時間一定是滿足 Timer 階段 中回調預設的時間 > 消息循環所保存的時間 , 所以 console.log(1) 得到執行, 輸出 1. 總的來說, 結果就是 2 1.
所以, 輸出不穩定的原因就取決于進入 Timer 階段 的時間是否和執行 setTimeout 的時間在 1ms 內. 如果把代碼改成如下, 則一定會得到穩定的輸出:
require("fs").readFile("my-file-path.txt", () => { setImmediate(() => console.log(2)) setTimeout(() => console.log(1)) });
這是因為消息循環在 Pneding I/O Phase 才向 Timer 和 Check 隊列插入回調. 這時按照消息循環的執行順序, Check 一定在 Timer 之前執行。
從性能角度講, setTimeout 的處理是在 Timer Phase, 其中 min heap 保存了 timer 的回調, 因此每執行一個回調的同時都會涉及到堆調整. 而 setImmediate 僅僅是清空一個隊列. 效率自然會高很多.
再從執行時機上講. setTimeout(..., 0) 和 setImmediate 完全屬于兩個階段.
5. 一個實際例子演示下面以一段代碼來說明nodejs運行JavaScript的機制。
如下面一段代碼:
setTimeout(() => { // settimeout1 console.log("1") new Promise((resolve) => { console.log("2"); resolve(); }) // Promise3 .then(() => { console.log("3") }) new Promise((resolve)=> { console.log("4"); resolve()}) // Promise4 .then(() => { console.log("5") }) setTimeout(() => { // settimeout3 console.log("6") setTimeout(() => { // settimeout5 console.log("7") new Promise((resolve) => { console.log("8"); resolve() }) // Promise5 .then( () => { console.log("9") }) new Promise((resolve) => { console.log("10"); resolve() }) // Promise6 .then(() => { console.log("11") }) }) setTimeout(() => { console.log("12") }, 0) // settimeout6 }) setTimeout(() => { console.log("13") }, 0) // settimeout4 }) setTimeout(() => { console.log("14") }, 0) // settimeout2 new Promise((resolve) => { console.log("15"); resolve() }) // Promise1 .then( ()=> { console.log("16") }) new Promise((resolve) => { console.log("17"); resolve() }) // Promise2 .then(() => { console.log("18") })
上面代碼執行過程:
node初始化
執行JavaScript代碼
遇到setTimeout, 把回調函數放到Timer隊列中,記為settimeout1
遇到setTimeout, 把回調函數放到Timer隊列中,記為settimeout2
遇到Promise,執行,輸出15,把回調函數放到微任務隊列,記為Promise1
遇到Promise,執行,輸出17,把回調函數放到微任務隊列,記為Promise2
代碼執行結束,此階段輸出結果:15 17
沒有process.nextTick回調,略過
執行微任務
檢查微任務隊列是否有可執行回調,此時隊列有2個回調:Promise1、Promise2
執行Promise1回調,輸出16
執行Promise2回調,輸出18
此階段輸出結果:16 18
進入第一次事件循環
進入Timer階段
檢查Timer隊列是否有可執行的回調,此時隊列有2個回調:settimeout1、settimeout2
執行settimeout1回調:
輸出1、2、4
添加了2個微任務,記為Promise3、Promise4
添加了2個Timer任務,記為settimeout3、settimeout4
執行settimeout2回調,輸出14
Timer隊列任務執行完畢
沒有process.nextTick回調,略過
檢查微任務隊列是否有可執行回調,此時隊列有2個回調:Promise3、Promise4
按順序執行2個微任務,輸出3、5
此階段輸出結果:1 2 4 14 3 5
Pending I/O Callback階段沒有任務,略過
進入 Poll 階段
檢查是否存在尚未完成的回調,此時有2個回調:settimeout3、settimeout4
執行settimeout3回調
輸出6
添加了2個Timer任務,記為settimeout5、settimeout6
執行settimeout4回調,輸出13
沒有process.nextTick回調,略過
沒有微任務,略過
此階段輸出結果:6 13
check、closing階段沒有任務,略過
檢查是否還有活躍的handles(定時器、IO等事件句柄),有,繼續下一輪事件循環
進入第二次事件循環
進入Timer階段
檢查Timer隊列是否有可執行的回調,此時隊列有2個回調:settimeout5、settimeout6
執行settimeout5回調:
輸出7、 8、10
添加了2個微任務,記為Promise5、Promise6
執行settimeout6回調,輸出12
沒有process.nextTick回調,略過
檢查微任務隊列是否有可執行回調,此時隊列有2個回調:Promise5、Promise6
按順序執行2個微任務,輸出9、11
此階段輸出結果:7 8 10 12 9 11
Pending I/O Callback、Poll、check、closing階段沒有任務,略過
檢查是否還有活躍的handles(定時器、IO等事件句柄),沒有了,結束事件循環,退出程序
程序執行結束,輸出結果:15 17 16 18 1 2 4 14 3 5 6 13 7 8 10 12 9 11
參考資料深入分析Node.js事件循環與消息隊列
剖析nodejs的事件循環
Node中的事件循環和異步API
Node.js Event Loop nodejs官網
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/109305.html
摘要:的單線程,與它的用途有關。特點的顯著特點異步機制事件驅動。隊列的讀取輪詢線程,事件的消費者,的主角。它將不同的任務分配給不同的線程,形成一個事件循環,以異步的方式將任務的執行結果返回給引擎。 這兩天跟同事同事討論遇到的一個問題,js中的event loop,引出了chrome與node中運行具有setTimeout和Promise的程序時候執行結果不一樣的問題,從而引出了Nodejs的...
摘要:主線程不斷重復上面的三步,此過程也就是常說的事件循環。所以主線程代碼執行時間過長,會阻塞事件循環的執行。參考資料這一次,徹底弄懂執行機制任務隊列的順序機制事件循環搞懂異步事件輪詢與中的事件循環 1. 說明 讀過本文章后,您能知道: JavaScript代碼在瀏覽器中的執行機制和事件循環 面試中經常遇到的代碼輸出順序問題 首先通過一段代碼來驗證你是否了解代碼輸出順序,如果你不知道輸出...
摘要:使用了一個事件驅動非阻塞式的模型,使其輕量又高效。的包管理器,是全球最大的開源庫生態系統。按照這個定義,之前所述的阻塞,非阻塞,多路復用信號驅動都屬于同步。 系列文章 Nodejs高性能原理(上) --- 異步非阻塞事件驅動模型Nodejs高性能原理(下) --- 事件循環詳解 前言 終于開始我nodejs的博客生涯了,先從基本的原理講起.以前寫過一篇瀏覽器執行機制的文章,和nodej...
摘要:瀏覽器與的異同,以及部分機制有人對部分迷惑,本身構造函數是同步的,是異步。瀏覽器的的已全部分析完成,過程中引用阮一峰博客,知乎,部分文章內容,侵刪。 瀏覽器與NodeJS的EventLoop異同,以及部分機制 PS:有人對promise部分迷惑,Promise本身構造函數是同步的,.then是異步。---- 2018/7/6 22:35修改 javascript 是一門單線程的腳本...
閱讀 3149·2021-11-22 13:54
閱讀 3435·2021-11-15 11:37
閱讀 3598·2021-10-14 09:43
閱讀 3496·2021-09-09 11:52
閱讀 3595·2019-08-30 15:53
閱讀 2457·2019-08-30 13:50
閱讀 2054·2019-08-30 11:07
閱讀 884·2019-08-29 16:32