小編給大家分享一下JavaScript在nodejs環(huán)境下執(zhí)行機制和事件循環(huán)的示例,希望大家閱讀完這篇文章后大所收獲,下面讓我們一起去探討吧!
成都創(chuàng)新互聯(lián)公司從2013年創(chuàng)立,先為烏當?shù)确?wù)建站,烏當?shù)鹊仄髽I(yè),進行企業(yè)商務(wù)咨詢服務(wù)。為烏當企業(yè)網(wǎng)站制作PC+手機+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問題。
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啟動過程可以分為以下步驟:
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)中的每一個 階段.
這是消息循環(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í)行.
執(zhí)行除了close callbacks
、setTimeout()
、setInterval()
、setImmediate()
回調(diào)之外幾乎所有回調(diào),比如說TCP連接發(fā)生錯誤
、 fs.read
, socket
等 IO 操作的回調(diào)函數(shù), 同時也包括各種 error 的回調(diào).
系統(tǒng)內(nèi)部的一些調(diào)用。
這是整個消息循環(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
隊列為空的時候,這里有兩種情況。
setImmediate()
設(shè)定了回調(diào),那么事件循環(huán)直接結(jié)束poll
階段進入check
階段來執(zhí)行check
隊列里的回調(diào)。如果代碼沒有被設(shè)定setImmediate()
設(shè)定回調(diào):
Poll階段,當js層代碼注冊的事件回調(diào)都沒有返回的時候,事件循環(huán)會暫時阻塞在poll階段,解除阻塞的條件:
- 在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', ...)
. 用于資源清理.
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í)行2.2、進入Pending I/O Callback
階段
Pending I/O Callback
的回調(diào),如果有,執(zhí)行回調(diào)。如果沒有退出該階段process.nextTick
任務(wù),如果有,全部執(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í)行。
事件循環(huán)隊列先保證所有的process.nextTick
回調(diào),然后將所有的Promise
回調(diào)追加在后面,最終在每個階段結(jié)束的時候一次性拿出來執(zhí)行。
此外,process.nextTick
和Promise
回調(diào)的數(shù)量是受限制的,也就是說,如果一直往這個隊列中加入回調(diào),那么整個事件循環(huán)就會被卡住
。
這兩個方法的回調(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 完全屬于兩個階段.
下面以一段代碼來說明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
看完了這篇文章,相信你對JavaScript在nodejs環(huán)境下執(zhí)行機制和事件循環(huán)的示例有了一定的了解,想了解更多相關(guān)知識,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝各位的閱讀!