首頁  >  文章  >  web前端  >  JavaScript在nodejs環境下執行機制和事件循環

JavaScript在nodejs環境下執行機制和事件循環

不言
不言轉載
2019-04-02 10:59:592506瀏覽

這篇文章帶給大家的內容是關於JavaScript高階函數的用法介紹,有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

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 的運作環境。
  1. 呼叫 performance_node_start 方法,對 nodejs 進行效能統計。
  2. openssl設定的判斷。
  3. 呼叫v8_platform.Initialize,初始化 libuv 執行緒池。
  4. 呼叫 V8::Initialize,初始化 V8 環境。
  5. 建立一個nodejs運行實例。
  6. 啟動上一步建立好的實例。
  7. 開始執行js文件,同步程式碼執行完畢後,進入事件循環。
  8. 在沒有任何可監聽的事件時,銷毀 nodejs 實例,程式執行完畢。

JavaScript在nodejs環境下執行機制和事件循環

3. nodejs的事件循環詳解

Nodejs 將訊息循環又細分為6 個階段(官方叫做Phase), 每個階段都會有一個類似於隊列的結構, 存儲著該階段需要處理的回調函數.

Nodejs 為了防止某個階段任務太多, 導致後續的階段發生飢餓的現象, 所以訊息循環的每一個迭代(iterate) 中, 每個階段執行回調都有個最大數量. 如果超過數量的話也會強行結束當前階段而進入下一個階段. 這條規則適用於訊息循環中的每一個階段.

3.1 Timer 階段

這是訊息循環的第一個階段, 用一個

for 循環處理所有setTimeoutsetInterval 的回呼.

這些回呼被保存在一個最小堆(min heap) 中. 這樣引擎只需要每次判斷頭元素, 如果符合條件就拿出來執行, 直到遇到一個不符合條件或佇列空了, 才結束Timer Phase.

Timer 階段中判斷某個回呼是否符合條件的方法也很簡單. 訊息循環每次進入Timer 的時候都會保存當時的系統時間,然後只要看上述最小堆中的回呼函數設定的啟動時間是否超過進入Timer 時保存的時間, 如果超過就拿出來執行.

#3.2 Pending I/O Callback 階段

#執行除了

close callbackssetTimeout()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階段,解除封鎖的條件:

  1. 在poll階段執行的時候,會傳入一個timeout逾時時間,而這個逾時時間就是poll階段的最大阻塞時間。
  2. timeout時間未到的時候,如果有事件返回,就執行該事件註冊的回呼函數。 timeout逾時時間到了,則退出poll階段,執行下一個階段。

這個timeout 設定為多少合適呢? 答案就是Timer Phase 中最近要執行的回呼啟動時間到現在的差值, 假設這個差值是detal. 因為Poll Phase 後面沒有等待執行的回調了. 所以這裡最多等待delta 時長, 如果期間有事件喚醒了消息循環, 那麼就繼續下一個Phase 的工作; 如果期間什麼都沒發生, 那麼到了timeout 後, 消息循環依然要進入後面的Phase, 讓下一個迭代的Timer Phase 也能夠得到執行.
Nodejs 就是透過Poll Phase, 對IO 事件的等待和內核非同步事件的到達來驅動整個訊息循環的.

3.5 Check  階段

這個階段只處理setImmediate 的回呼函數.
那麼為什麼這裡要有專門一個處理setImmediate 的階段呢? 簡單來說, 是因為Poll 階段可能設定一些回呼, 希望在Poll 階段後運行. 所以在Poll 階段後面增加了這個Check 階段.

3.6 Close Callbacks 階段

專門處理一些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.nextTickPromise回调的数量是受限制的,也就是说,如果一直往这个队列中加入回调,那么整个事件循环就会被卡住

JavaScript在nodejs環境下執行機制和事件循環

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

JavaScript在nodejs環境下執行機制和事件循環

【相關推薦:

JavaScript影片教學

### ###

以上是JavaScript在nodejs環境下執行機制和事件循環的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除