從一道執行題,瞭解Node中JS執行機制

ntscshen發表於2018-06-05

與瀏覽器環境有何不同

node環境和瀏覽器環境,表現出來的事件迴圈狀態,大體表現一致 唯一不同的是:

  1. JS引擎存在 monitoring process 程式,會持續不斷的檢查主執行緒執行為空,一旦為空,就會去 callback queue 中檢查是否有等待被呼叫的函式。(只有巨集任務和微任務兩個佇列)
  2. node 中是依靠 libuv 引擎實現,我們書寫的 js 程式碼有 V8 引擎分析後去呼叫對應的 nodeAPI ,這些 api 最後由 libuv 引擎驅動,在 libuv 引擎中有一套自己的模型,把不同的事件放在不同的佇列中等待主執行緒執行。( 模型中有6種巨集任務佇列和1種微任務佇列 )

Node事件迴圈的幾個階段

// libuv引擎中的事件模型,在每個模型後面都新增了一些說明

   ┌───────────────────────────────────────────────────────┐
┌─>│        timers       │ setTimeout/setInterval的回撥
│  └──────────┬────────────────────────────────────────────┘
│             ↓
│  ┌──────────┴────────────────────────────────────────────┐
│  │   pending callbacks │ 處理網路、流、tcp的錯誤回撥
│  └──────────┬────────────────────────────────────────────┘
│             ↓
│  ┌──────────┴────────────────────────────────────────────┐
│  │     idle, prepare   │ 只在node內部使用
│  └──────────┬────────────────────────────────────────────┘
│             ↓                                                      ┌───────────────┐
│  ┌──────────┴────────────────────────────────────────────┐         │   incoming:   │
│  │         poll        │ 執行poll中的i/o佇列,檢查定時器是否到時  <------│   connections,
│  └──────────┬────────────────────────────────────────────┘         │   data, etc.  │
│             ↓                                                      └───────────────┘
│  ┌──────────┴────────────────────────────────────────────┐
│  │        check        │ 存放setImmediate回撥
│  └──────────┬────────────────────────────────────────────┘
│             ↓
│  ┌──────────┴────────────────────────────────────────────┐
└──┤    close callbacks  │ 關閉的回撥(socket.on('close')...)
   └───────────────────────────────────────────────────────┘
複製程式碼

Node事件迴圈中的幾個階段

官方的event-loop-timers-and-nexttick更詳細的說明

  1. times:這個階段執行定時器佇列中的回撥函式 ( setTimeoutsetInterval )
  2. pending callback:這個階段執行幾乎所有的回撥( 網路、流、tcp錯誤... )。除了,close 回撥、定時器回撥、setImmediate 回撥這3個規定好的階段
  3. idle,prepare:這個階段僅在內部使用( 可以暫不理會 )
  4. poll:等待新的I/O事件,node在特殊情況下會阻塞這裡,檢查定時器是否到時( 入口 )
  5. checksetImmediate() 的回撥會在這個階段執行
  6. close callbacks:例如 socket.on('close', ...)
  7. process.nextTick.then() 會在事件迴圈的階段切換過程中執行

說了一堆概念,來一起看看下面這段程式碼

(function test() {
  setTimeout(function () { console.log(4) }, 0);
  new Promise(function (resolve, reject) {
    console.log(1);
    for (var i = 0; i < 10000; i++) {
      i == 9999 && resolve();
    }
    console.log(2);
  }).then(function () {
    console.log(5);
  });
  console.log(3);
})();
// 這段程式碼是不是很熟悉
// 最終結果1,2,3,5,4 和 瀏覽器中效果一致
複製程式碼

來點稍微高難度的

和上篇部落格 從一道執行題,瞭解瀏覽器中JS執行機制 中的程式碼一樣 (⊙﹏⊙)b

console.log(1)

setTimeout(() => {
  console.log(2)
  new Promise(resolve => {
    console.log(4)
    resolve()
  }).then(() => {
    console.log(5)
  })
})

new Promise(resolve => {
  console.log(7)
  resolve()
}).then(() => {
  console.log(8)
})

setTimeout(() => {
  console.log(9)
  new Promise(resolve => {
    console.log(11)
    resolve()
  }).then(() => {
    console.log(12)
  })
})
// 瀏覽器中的結果:1、7、8、2、4  , 5、9、11、12
// Node 中的結果:1、7、8、2、4  , 9、11、5、12
複製程式碼

解析如下:

  1. 在瀏覽器中 macro task 執行完成後,再次迴圈 巨集任務 的回撥佇列之前,會優先處理micro中的任務。因此結果是: 1、7、8、2、4、5、9、11、12
  2. Node 中有6個巨集任務佇列,事件迴圈首先進入 poll 階段。進入 poll 階段後檢視是否有設定的 timers ( 定時器 )時間到達,如果有一個或多個時間到達, Event Loop 將會跳過正常的迴圈流程,直接從 timers 階段執行,並執行 timers 回撥佇列,此時只有把 timers 階段的回撥佇列執行完畢後。才會走下一個階段,這也就是為什麼 setTimeout 中有 .then,而沒有被立即執行的原因,當 timers 階段的回撥佇列執行完畢後,切換到下一個階段這個過程中去觸發 微任務(process.nextTick.then) 。在階段與階段的切換之間。

再來一道基礎題

setTimeout(function () {
  console.log('setTimeout')
});
setImmediate(function () {
  console.log('setImmediate')
});
複製程式碼

執行結果:( setTimeout、setImmediate ) 或 ( setImmediate、setTimeout )

為什麼? setTimeout 在標準中預設的最小時間是4ms,如果開啟node和執行node程式碼的時間小於4ms,那麼程式碼解析完成後傳入 libuv 引擎,首先會進入 poll 階段,此時檢視設定的時間是否達到截止時間點,如果這個時間小於4ms( 沒有達到 ),那麼會走 check 階段,會觸發 setImmediate 再觸發 setTimeout。如果開啟node和執行node程式碼時間大於等於4ms,那麼就會先執行 setTimeout 後執行 setImmediate

在基礎上進化的經典題

setImmediate(() => {
  console.log('setImmediate1')
  setTimeout(() => {
    console.log('setTimeout1')
  }, 0);
})
setTimeout(()=>{
  process.nextTick(()=>console.log('nextTick'))
  console.log('setTimeout2')
  setImmediate(()=>{
    console.log('setImmediate2')
  })
},0);
複製程式碼

兩種情況 ( nextTick執行的位置:是在佇列切換時執行 )

  1. 如果 setImmediate 先執行:setImmediate1、setTimeout2、setTimeout1、nextTick、setImmediate2
  2. 如果 setTimeout 先執行:setTimeout2、nextTick、setImmediate1、setImmediate2、setTimeout1

setImmediate和process.nextTick的直譯

  1. Immediate立即執行的意思,其實際上是固定在 check 階段才會被執行。這個直譯的意義和 process.nextTick 才是最匹配的。
  2. node的開發者們也清楚這兩個方法的命名上存在一定的混淆,他們表示不會把這兩個方法的名字調換過來---因為有大量的node程式使用著這兩個方法,調換命名所帶來的好處與它的影響相比不值一提。

瞭解這些東西有什麼用?

  1. 可以使我們對非同步程式碼的執行順序有清晰的認知( 重要的 )
  2. 推遲任務執行
  3. 面試

總結

這些概念遠比想象中的要重要

  1. 為什麼 new Promise 第一個引數是同步執行的 ?學習Promise && 簡易實現Promise
  2. 瀏覽器 中的 JS 執行機制是什麼樣子的?從一道執行題,瞭解瀏覽器中JS執行機制

附:這篇部落格 也許 想表達 概念遠比想象中的要重要 (⊙﹏⊙)b

相關文章