溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

JavaScript在nodejs環(huán)境下執(zhí)行機(jī)制和事件循環(huán)的示例

發(fā)布時(shí)間:2020-12-02 11:07:40 來源:億速云 閱讀:260 作者:小新 欄目:web開發(fā)

小編給大家分享一下JavaScript在nodejs環(huán)境下執(zhí)行機(jī)制和事件循環(huán)的示例,希望大家閱讀完這篇文章后大所收獲,下面讓我們一起去探討吧!

1、說明

nodejs是單線程執(zhí)行的,同時(shí)它又是基于事件驅(qū)動的非阻塞IO編程模型。這就使得我們不用等待異步操作結(jié)果返回,就可以繼續(xù)往下執(zhí)行代碼。當(dāng)異步事件觸發(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啟動過程可以分為以下步驟:

  1. 調(diào)用platformInit方法 ,初始化 nodejs 的運(yùn)行環(huán)境。
  2. 調(diào)用 performance_node_start 方法,對 nodejs 進(jìn)行性能統(tǒng)計(jì)。
  3. openssl設(shè)置的判斷。
  4. 調(diào)用v8_platform.Initialize,初始化 libuv 線程池。
  5. 調(diào)用 V8::Initialize,初始化 V8 環(huán)境。
  6. 創(chuàng)建一個(gè)nodejs運(yùn)行實(shí)例。
  7. 啟動上一步創(chuàng)建好的實(shí)例。
  8. 開始執(zhí)行js文件,同步代碼執(zhí)行完畢后,進(jìn)入事件循環(huán)。
  9. 在沒有任何可監(jiān)聽的事件時(shí),銷毀 nodejs 實(shí)例,程序執(zhí)行完畢。

JavaScript在nodejs環(huán)境下執(zhí)行機(jī)制和事件循環(huán)的示例

3. nodejs的事件循環(huán)詳解

Nodejs 將消息循環(huán)又細(xì)分為 6 個(gè)階段(官方叫做 Phase), 每個(gè)階段都會有一個(gè)類似于隊(duì)列的結(jié)構(gòu), 存儲著該階段需要處理的回調(diào)函數(shù).

Nodejs 為了防止某個(gè) 階段 任務(wù)太多, 導(dǎo)致后續(xù)的 階段 發(fā)生饑餓的現(xiàn)象, 所以消息循環(huán)的每一個(gè)迭代(iterate) 中, 每個(gè) 階段 執(zhí)行回調(diào)都有個(gè)最大數(shù)量. 如果超過數(shù)量的話也會強(qiáng)行結(jié)束當(dāng)前 階段而進(jìn)入下一個(gè) 階段. 這一條規(guī)則適用于消息循環(huán)中的每一個(gè) 階段.

3.1 Timer 階段

這是消息循環(huán)的第一個(gè)階段, 用一個(gè) for 循環(huán)處理所有 setTimeoutsetInterval 的回調(diào).

這些回調(diào)被保存在一個(gè)最小堆(min heap) 中. 這樣引擎只需要每次判斷頭元素, 如果符合條件就拿出來執(zhí)行, 直到遇到一個(gè)不符合條件或者隊(duì)列空了, 才結(jié)束 Timer Phase.

Timer 階段中判斷某個(gè)回調(diào)是否符合條件的方法也很簡單. 消息循環(huán)每次進(jìn)入 Timer 的時(shí)候都會保存一下當(dāng)時(shí)的系統(tǒng)時(shí)間,然后只要看上述最小堆中的回調(diào)函數(shù)設(shè)置的啟動時(shí)間是否超過進(jìn)入 Timer 時(shí)保存的時(shí)間, 如果超過就拿出來執(zhí)行.

3.2 Pending I/O Callback 階段

執(zhí)行除了close callbacks、setTimeout()setInterval()、setImmediate()回調(diào)之外幾乎所有回調(diào),比如說TCP連接發(fā)生錯(cuò)誤、 fs.read, socket 等 IO 操作的回調(diào)函數(shù), 同時(shí)也包括各種 error 的回調(diào).

3.3 Idle, Prepare 階段

系統(tǒng)內(nèi)部的一些調(diào)用。

3.4 Poll 階段,重要階段

這是整個(gè)消息循環(huán)中最重要的一個(gè) 階段, 作用是等待異步請求和數(shù)據(jù),因?yàn)樗瘟苏麄€(gè)消息循環(huán)機(jī)制.

poll階段有兩個(gè)主要的功能:一是執(zhí)行下限時(shí)間已經(jīng)達(dá)到的timers的回調(diào),一是處理poll隊(duì)列里的事件。
注:Node的很多API都是基于事件訂閱完成的,比如fs.readFile,這些回調(diào)應(yīng)該都在poll階段完成。

當(dāng)事件循環(huán)進(jìn)入poll階段:

  • poll隊(duì)列不為空的時(shí)候,事件循環(huán)肯定是先遍歷隊(duì)列并同步執(zhí)行回調(diào),直到隊(duì)列清空或執(zhí)行回調(diào)數(shù)達(dá)到系統(tǒng)上限。
  • poll隊(duì)列為空的時(shí)候,這里有兩種情況。

    • 如果代碼已經(jīng)被setImmediate()設(shè)定了回調(diào),那么事件循環(huán)直接結(jié)束poll階段進(jìn)入check階段來執(zhí)行check隊(duì)列里的回調(diào)。
    • 如果代碼沒有被設(shè)定setImmediate()設(shè)定回調(diào):

      • 如果有被設(shè)定的timers,那么此時(shí)事件循環(huán)會檢查timers,如果有一個(gè)或多個(gè)timers下限時(shí)間已經(jīng)到達(dá),那么事件循環(huán)將繞回timers階段,并執(zhí)行timers的有效回調(diào)隊(duì)列。
      • 如果沒有被設(shè)定timers,這個(gè)時(shí)候事件循環(huán)是阻塞在poll階段等待事件回調(diào)被加入poll隊(duì)列。

Poll階段,當(dāng)js層代碼注冊的事件回調(diào)都沒有返回的時(shí)候,事件循環(huán)會暫時(shí)阻塞在poll階段,解除阻塞的條件:

  1. 在poll階段執(zhí)行的時(shí)候,會傳入一個(gè)timeout超時(shí)時(shí)間,該超時(shí)時(shí)間就是poll階段的最大阻塞時(shí)間。
  2. timeout時(shí)間未到的時(shí)候,如果有事件返回,就執(zhí)行該事件注冊的回調(diào)函數(shù)。timeout超時(shí)時(shí)間到了,則退出poll階段,執(zhí)行下一個(gè)階段。

這個(gè) timeout 設(shè)置為多少合適呢? 答案就是 Timer Phase 中最近要執(zhí)行的回調(diào)啟動時(shí)間到現(xiàn)在的差值, 假設(shè)這個(gè)差值是 detal. 因?yàn)?Poll Phase 后面沒有等待執(zhí)行的回調(diào)了. 所以這里最多等待 delta 時(shí)長, 如果期間有事件喚醒了消息循環(huán), 那么就繼續(xù)下一個(gè) Phase 的工作; 如果期間什么都沒發(fā)生, 那么到了 timeout 后, 消息循環(huán)依然要進(jìn)入后面的 Phase, 讓下一個(gè)迭代的 Timer Phase 也能夠得到執(zhí)行.
Nodejs 就是通過 Poll Phase, 對 IO 事件的等待和內(nèi)核異步事件的到達(dá)來驅(qū)動整個(gè)消息循環(huán)的.

3.5 Check  階段

這個(gè)階段只處理 setImmediate 的回調(diào)函數(shù).
那么為什么這里要有專門一個(gè)處理 setImmediate 的 階段 呢? 簡單來說, 是因?yàn)?Poll 階段可能設(shè)置一些回調(diào), 希望在 Poll 階段 后運(yùn)行. 所以在 Poll 階段 后面增加了這個(gè) Check 階段.

3.6 Close Callbacks 階段

專門處理一些 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、進(jìn)入事件循環(huán)

2.1、進(jìn)入Timer階段

  • 檢查Timer隊(duì)列是否有到期的Timer的回調(diào),如果有,將到期的所有Timer回調(diào)按照TimerId升序執(zhí)行
  • 檢查是否有process.nextTick任務(wù),如果有,全部執(zhí)行
  • 檢查是否有微任務(wù)(promise),如果有,全部執(zhí)行
  • 退出該階段

2.2、進(jìn)入Pending I/O Callback階段

  • 檢查是否有Pending I/O Callback的回調(diào),如果有,執(zhí)行回調(diào)。如果沒有退出該階段
  • 檢查是否有process.nextTick任務(wù),如果有,全部執(zhí)行
  • 檢查是否有微任務(wù)(promise),如果有,全部執(zhí)行
  • 退出該階段

2.3、進(jìn)入idle,prepare階段

這個(gè)階段與JavaScript關(guān)系不大,略過

2.4、進(jìn)入Poll階段

首先檢查是否存在尚未完成的回調(diào),如果存在,分如下兩種情況:

第一種情況:有可執(zhí)行的回調(diào)

執(zhí)行所有可用回調(diào)(包含到期的定時(shí)器還有一些IO事件等)

檢查是否有process.nextTick任務(wù),如果有,全部執(zhí)行

檢查是否有微任務(wù)(promise),如果有,全部執(zhí)行

退出該階段

第二種情況:沒有可執(zhí)行的回調(diào)

檢查是否有immediate回調(diào),如果有,退出Poll階段。如果沒有,阻塞在此階段,等待新的事件通知

如果不存在尚未完成的回調(diào),退出Poll階段

2.5、進(jìn)入check階段

如果有immediate回調(diào),則執(zhí)行所有immediate回調(diào)

檢查是否有process.nextTick任務(wù),如果有,全部執(zhí)行

檢查是否有微任務(wù)(promise),如果有,全部執(zhí)行

退出該階段

2.6、進(jìn)入closing階段

如果有immediate回調(diào),則執(zhí)行所有immediate回調(diào)

檢查是否有process.nextTick任務(wù),如果有,全部執(zhí)行

檢查是否有微任務(wù)(promise),如果有,全部執(zhí)行

退出該階段

3、檢查是否有活躍的handles(定時(shí)器、IO等事件句柄)

如果有,繼續(xù)下一輪事件循環(huán)

如果沒有,結(jié)束事件循環(huán),退出程序

注意:

事件循環(huán)的每一個(gè)子階段退出之前都會按順序執(zhí)行如下過程:

檢查是否有 process.nextTick 回調(diào),如果有,全部執(zhí)行。

檢查是否有 微任務(wù)(promise),如果有,全部執(zhí)行。

4.1 關(guān)于Promise和process.nextTick

事件循環(huán)隊(duì)列先保證所有的process.nextTick回調(diào),然后將所有的Promise回調(diào)追加在后面,最終在每個(gè)階段結(jié)束的時(shí)候一次性拿出來執(zhí)行。

此外,process.nextTickPromise回調(diào)的數(shù)量是受限制的,也就是說,如果一直往這個(gè)隊(duì)列中加入回調(diào),那么整個(gè)事件循環(huán)就會被卡住

JavaScript在nodejs環(huán)境下執(zhí)行機(jī)制和事件循環(huán)的示例

4.2 關(guān)于setTimeout(…, 0) 和 setImmediate

這兩個(gè)方法的回調(diào)到底誰快?

如下面的例子:

setImmediate(() => console.log(2))
setTimeout(() => console.log(1))

使用nodejs多次執(zhí)行后,發(fā)現(xiàn)輸出結(jié)果有時(shí)是1 2,有時(shí)是2 1。

對于多次執(zhí)行輸出結(jié)果不同,需要了解事件循環(huán)的基礎(chǔ)問題。

首先,Nodejs啟動,初始化環(huán)境后加載我們的JS代碼(index.js).發(fā)生了兩件事(此時(shí)尚未進(jìn)入消息循環(huán)環(huán)節(jié)):

setImmediate 向 Check 階段 中添加了回調(diào) console.log(2);

setTimeout 向 Timer 階段 中添加了回調(diào) console.log(1)

這時(shí)候, 要初始化階段完畢, 要進(jìn)入 Nodejs 消息循環(huán)了。

為什么會有兩種輸出呢? 接下來一步很關(guān)鍵:

當(dāng)執(zhí)行到 Timer 階段 時(shí), 會發(fā)生兩種可能. 因?yàn)槊恳惠喌鷦倓傔M(jìn)入 Timer 階段 時(shí)會取系統(tǒng)時(shí)間保存起來, 以 ms(毫秒) 為最小單位.

如果 Timer 階段 中回調(diào)預(yù)設(shè)的時(shí)間 > 消息循環(huán)所保存的時(shí)間, 則執(zhí)行 Timer 階段 中的該回調(diào). 這種情況下先輸出 1, 直到 Check 階段 執(zhí)行后,輸出2.總的來說, 結(jié)果是 1 2.

如果運(yùn)行比較快, Timer 階段 中回調(diào)預(yù)設(shè)的時(shí)間可能剛好等于消息循環(huán)所保存的時(shí)間, 這種情況下, Timer 階段 中的回調(diào)得不到執(zhí)行, 則繼續(xù)下一個(gè) 階段. 直到 Check 階段, 輸出 2. 然后等下一輪迭代的 Timer 階段, 這時(shí)的時(shí)間一定是滿足 Timer 階段 中回調(diào)預(yù)設(shè)的時(shí)間 > 消息循環(huán)所保存的時(shí)間 , 所以 console.log(1) 得到執(zhí)行, 輸出 1. 總的來說, 結(jié)果就是 2 1.

所以, 輸出不穩(wěn)定的原因就取決于進(jìn)入 Timer 階段 的時(shí)間是否和執(zhí)行 setTimeout 的時(shí)間在 1ms 內(nèi). 如果把代碼改成如下, 則一定會得到穩(wěn)定的輸出:

require('fs').readFile('my-file-path.txt', () => {
 setImmediate(() => console.log(2))
 setTimeout(() => console.log(1))
});

這是因?yàn)橄⒀h(huán)在 Pneding I/O Phase 才向 Timer 和 Check 隊(duì)列插入回調(diào). 這時(shí)按照消息循環(huán)的執(zhí)行順序, Check 一定在 Timer 之前執(zhí)行。

從性能角度講, setTimeout 的處理是在 Timer Phase, 其中 min heap 保存了 timer 的回調(diào), 因此每執(zhí)行一個(gè)回調(diào)的同時(shí)都會涉及到堆調(diào)整. 而 setImmediate 僅僅是清空一個(gè)隊(duì)列. 效率自然會高很多.

再從執(zhí)行時(shí)機(jī)上講. setTimeout(..., 0) 和 setImmediate 完全屬于兩個(gè)階段.

5. 一個(gè)實(shí)際例子演示

下面以一段代碼來說明nodejs運(yùn)行JavaScript的機(jī)制。

如下面一段代碼:

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隊(duì)列中,記為settimeout1

遇到setTimeout, 把回調(diào)函數(shù)放到Timer隊(duì)列中,記為settimeout2

遇到Promise,執(zhí)行,輸出15,把回調(diào)函數(shù)放到微任務(wù)隊(duì)列,記為Promise1

遇到Promise,執(zhí)行,輸出17,把回調(diào)函數(shù)放到微任務(wù)隊(duì)列,記為Promise2

代碼執(zhí)行結(jié)束,此階段輸出結(jié)果:15 17

沒有process.nextTick回調(diào),略過

執(zhí)行微任務(wù)

檢查微任務(wù)隊(duì)列是否有可執(zhí)行回調(diào),此時(shí)隊(duì)列有2個(gè)回調(diào):Promise1、Promise2

執(zhí)行Promise1回調(diào),輸出16

執(zhí)行Promise2回調(diào),輸出18

此階段輸出結(jié)果:16 18

進(jìn)入第一次事件循環(huán)

進(jìn)入Timer階段

檢查Timer隊(duì)列是否有可執(zhí)行的回調(diào),此時(shí)隊(duì)列有2個(gè)回調(diào):settimeout1、settimeout2

執(zhí)行settimeout1回調(diào):

輸出1、2、4

添加了2個(gè)微任務(wù),記為Promise3、Promise4

添加了2個(gè)Timer任務(wù),記為settimeout3、settimeout4

執(zhí)行settimeout2回調(diào),輸出14

Timer隊(duì)列任務(wù)執(zhí)行完畢

沒有process.nextTick回調(diào),略過

檢查微任務(wù)隊(duì)列是否有可執(zhí)行回調(diào),此時(shí)隊(duì)列有2個(gè)回調(diào):Promise3、Promise4

按順序執(zhí)行2個(gè)微任務(wù),輸出3、5

此階段輸出結(jié)果:1 2 4 14 3 5

Pending I/O Callback階段沒有任務(wù),略過

進(jìn)入 Poll 階段

檢查是否存在尚未完成的回調(diào),此時(shí)有2個(gè)回調(diào):settimeout3、settimeout4

執(zhí)行settimeout3回調(diào)

輸出6

添加了2個(gè)Timer任務(wù),記為settimeout5、settimeout6

執(zhí)行settimeout4回調(diào),輸出13

沒有process.nextTick回調(diào),略過

沒有微任務(wù),略過

此階段輸出結(jié)果:6 13

check、closing階段沒有任務(wù),略過

檢查是否還有活躍的handles(定時(shí)器、IO等事件句柄),有,繼續(xù)下一輪事件循環(huán)

進(jìn)入第二次事件循環(huán)

進(jìn)入Timer階段

檢查Timer隊(duì)列是否有可執(zhí)行的回調(diào),此時(shí)隊(duì)列有2個(gè)回調(diào):settimeout5、settimeout6

執(zhí)行settimeout5回調(diào):

輸出7、 8、10

添加了2個(gè)微任務(wù),記為Promise5、Promise6

執(zhí)行settimeout6回調(diào),輸出12

沒有process.nextTick回調(diào),略過

檢查微任務(wù)隊(duì)列是否有可執(zhí)行回調(diào),此時(shí)隊(duì)列有2個(gè)回調(diào):Promise5、Promise6

按順序執(zhí)行2個(gè)微任務(wù),輸出9、11

此階段輸出結(jié)果:7 8 10 12 9 11

Pending I/O Callback、Poll、check、closing階段沒有任務(wù),略過

檢查是否還有活躍的handles(定時(shí)器、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í)行機(jī)制和事件循環(huán)的示例

看完了這篇文章,相信你對JavaScript在nodejs環(huán)境下執(zhí)行機(jī)制和事件循環(huán)的示例有了一定的了解,想了解更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI