從例項程式碼講解Node.js Event loop執行機制(1.0.1)

css_Machinist發表於2019-01-18

2018-03-23 星期五 農曆 二月初七戊戌年 【狗年】乙卯月 甲寅日
宜:  裁衣、經絡、伐木、開柱眼、拆卸、修造、動土、上樑、合脊、合壽木、入殮、除服、成服、移柩、破土
忌:  祭祀、嫁娶、出行、上樑、掘井

本文采用 自問自答的形式 配合程式碼(執行結果)來講解 event loop 的執行機制

執行環境 :  node.js-8.10.0  WebStorm 2017.2.3 

example 1

console.log(1);
console.log(2);
setTimeout(function(){
    console.log(3)
})
setTimeout(function(){
    console.log(4);
})
console.log(5)複製程式碼

 執行:輸入結果 顯示如下

從例項程式碼講解Node.js Event loop執行機制(1.0.1)

現在我們來例項分析下 這個程式碼執行結果為什麼是這樣的 : 

   Node是基於單執行緒的(主要主執行緒是單執行緒,用來執行同步任務。等遇到非同步任務的時候,會呼叫另外一條非同步執行緒來處理非同步任務,如:setTimeout 這些會影響主執行緒執行的,需要等待一段時間)   

程式碼執行分析 如下:   主執行緒開始執行,自上往下,先是開始執行同步任務, 依次執行

console.log(1)
console.log(2) 
複製程式碼

然後遇到 非同步任務

setTimeout(function(){
    console.log(3)
})
setTimeout(function(){
    console.log(4);
})複製程式碼

這時候 Node 會把 這些非同步任務 push 到一個 非同步執行棧 stack  裡面  

然後繼續執行 主執行緒的 任務

console.log(5) 複製程式碼

等到所有的同步任務任務,這時候 Node  開始執行  非同步佇列  棧 stack 裡面的非同步任務 

按照 堆 stack 的特徵 先進後出的特徵   會依次開始執行 棧裡面的非同步任務  

等待他們執行完畢以後,會生成一個 巨集  任務佇列(下面會有詳細的介紹),按照執行前後順序來新增到這個佇列中,

佇列遵循先進先出的規則,開始依次執行 列隊

執行結果如下:

3
4複製程式碼

現在來更深入的瞭解下 Node.js event loop 機制

當Node.js啟動時會初始化event loop, 每一個event loop都會包含按如下順序六個迴圈階段,

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘複製程式碼
  • timers 階段: 這個階段執行setTimeout(callback) and setInterval(callback)預定的callback;
  • I/O callbacks 階段: 執行除了 close事件的callbacks、被timers(定時器,setTimeout、setInterval等)設定的callbacks、setImmediate()設定的callbacks之外的callbacks;
  • idle, prepare 階段: 僅node內部使用;
  • poll 階段: 獲取新的I/O事件, 適當的條件下node將阻塞在這裡;
  • check 階段: 執行setImmediate() 設定的callbacks;
  • close callbacks 階段: 比如socket.on(‘close’, callback)的callback會在這個階段執行.

每一個階段都有一個裝有callbacks的fifo queue(佇列),當event loop執行到一個指定階段時,
node將執行該階段的fifo queue(佇列),當佇列callback執行完或者執行callbacks數量超過該階段的上限時,
event loop會轉入下一下階段.

注意上面六個階段都不包括 process.nextTick() (稍後會在巨集任務和微任務)

example 2

setTimeout(() => {
  console.log(`setTimeout`)
}, 0)

setImmediate(() => {
  console.log(`setImmediate`)
})複製程式碼

執行結果:

setImmediate
setTimeout
複製程式碼

或者:

setTimeout
setImmediate
複製程式碼

為什麼結果不確定呢?

解釋:setTimeout/setInterval 的第二個引數取值範圍是:[1, 2^31 – 1],如果超過這個範圍則會初始化為 1,即 setTimeout(fn, 0) === setTimeout(fn, 1)。我們知道 setTimeout 的回撥函式在 timer 階段執行,setImmediate 的回撥函式在 check 階段執行,event loop 的開始會先檢查 timer 階段,但是在開始之前到 timer 階段會消耗一定時間,所以就會出現兩種情況:

  1. timer 前的準備時間超過 1ms,滿足 loop->time >= 1,則執行 timer 階段(setTimeout)的回撥函式
  2. timer 前的準備時間小於 1ms,則先執行 check 階段(setImmediate)的回撥函式,下一次 event loop 執行 timer 階段(setTimeout)的回撥函式

example 3

const fs = require(`fs`)

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log(`setTimeout`)
  }, 0)

  setImmediate(() => {
    console.log(`setImmediate`)
  })
})
複製程式碼

執行結果:

setImmediate
setTimeout
複製程式碼

解釋:fs.readFile 的回撥函式執行完後:

  1. 註冊 setTimeout 的回撥函式到 timer 階段
  2. 註冊 setImmediate 的回撥函式到 check 階段
  3. event loop 從 pool 階段出來繼續往下一個階段執行,恰好是 check 階段,所以 setImmediate 的回撥函式先執行
  4. 本次 event loop 結束後,進入下一次 event loop,執行 setTimeout 的回撥函式

所以,在 I/O Callbacks 中註冊的 setTimeout 和 setImmediate,永遠都是 setImmediate 先執行。

巨集任務和微任務

這兩個概念屬於對非同步任務的分類,不同的API註冊的非同步任務會依次進入自身對應的佇列中,然後等待Event Loop將它們依次壓入執行棧中執行。

task主要包含:setTimeoutsetIntervalsetImmediateI/OUI互動事件

microtask主要包含:Promiseprocess.nextTick

microtask 會優先 task 執行 

一般 Event loop 會先清空 microtask 佇列裡面的任務,然後才回去 執行 task 裡面的 非同步任務

example 4

console.log(1);
console.log(2);
setImmediate(function(){
    console.log(4);
})
setTimeout(function(){
    console.log(3)
})
process.nextTick(function(){
    console.log(`process.nextTick`)
})
Promise.resolve().then(function () {
    console.log(`Promise`)
})複製程式碼

執行結果如下: 

1 
2
process.nextTick 
Promise 
3 
4複製程式碼

先依次執行同步任務 console.log 輸出

1
2複製程式碼

然後開始執行非同步任務 stack 裡面的任務 先微任務在巨集任務

輸出

process.nextTick 
Promise 複製程式碼

最後等待微任務執行完畢以後,在執行最後的巨集任務

3 
4複製程式碼

待續。。。。。。

相關文章