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