淺談Node.js的事件環(event loop)

zhangyuxiang1226發表於2018-07-29

1.nodejs 為什麼要存在一個event loop的事件處理機制

nodejs 具有事件驅動和非阻塞但執行緒的特點,使相關應用變得比較輕量和高效。當應用程式需要相關I/O操作時,執行緒並不會阻塞,而是把I/O操作移交給底層類庫(如:libuv)。此時nodejs執行緒會去處理其他的任務,當底層庫處理完相關的I/O操作後,會將主動權再次交還給nodejs執行緒。因此event loop的作用就是起到排程執行緒的作用,如當底層類庫處理I/O操作後排程nodejs單執行緒處理後續的工作。也就是說當nodejs 程式啟動的時候,它會開啟一個event loop以實現非同步的api排程、schedule timers 、回撥process.nextTick()。

從上也可以看出nodejs 雖說是單執行緒,但是在底層類庫處理非同步操作的時候仍然是多執行緒。

2.引出問題

在node環境中我們執行如下程式碼,會出現怎麼樣的執行結果?

let fs = require('fs');
setTimeout(function(){
    Promise.resolve().then(()=>{
        console.log('then2');
    })
},0);
Promise.resolve().then(()=>{
    console.log('then1');
});
fs.readFile('./gitigore',function(){
    process.nextTick(function(){
        console.log('nextTick')
    })
    setImmediate(()=>{
        console.log('setImmediate')
    });
});
複製程式碼

在node環境的執行結果是

then1
then2
nextTick
setImmediate
複製程式碼

3.開始事件迴圈之前,nodejs初始化

產出這樣的結果,來源於Node.js對事件的迴圈操作順序。在Node.js的官方文件中,對初始化event loop有這樣的描述 The Node.js Event Loop, Timers, and process.nextTick()

淺談Node.js的事件環(event loop)
-當Node.js啟動的時候,他會初始化Event Loop,處理提供的輸入指令碼,這可能會使非同步API呼叫,呼叫timers,或呼叫process.nextTick,然後開始處理事件迴圈,下面是一個經典的事件迴圈操作順序

   ┌───────────────────────────────────┐
┌─>│timers(計時器)執行                  │
│  |setTimeout以及setInterval的回撥     │
│  └──────────┬────────────────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│   處理網路,流,TCP的錯誤  │  
│  │      callback      │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  │    node內部使用        │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │poll(輪詢)            │<─────┤  connections, │
│  │ 執行poll中的i/o佇列檢查 │      │data, etc.    │
│  │定時器是否到時          │      └───────────────┘
│  └──────────┬────────────┘          
│  ┌──────────┴────────────┐      
│  │        check          │
│  │  存放setImmediate回撥  │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   │ 關閉的回撥例如         │
   │ socket.on('close')    │
   └───────────────────────┘
複製程式碼

其中,每個盒子都是 Event Loop的一個階段,當Event Loop進入到某個階段的時候,就會將該階段佇列裡的回撥拿出來執行,直到佇列為空。

幾個佇列

Timers Queue - 計時器佇列
I/O Queue - 輸入輸出佇列
Check Queue - 檢查佇列
Close Queue - guangbi 佇列
複製程式碼

除了上面迴圈階段的任務型別,還有瀏覽器和nodejs共有的微任務(micro task)和node的 process.nextTick。分別稱其對應的佇列為MircoTask Queue和NextTick Queue

4. 開始迴圈之後:

依據上述6個階段依次執行,每次拿出當前階段的全部任務執行,清空NextTick佇列,清空微任務佇列,再執行下一階段,全部6個階段完畢後,進入下一輪的迴圈。

即用一張圖表述為

淺談Node.js的事件環(event loop)

  1. 結合程式碼
let fs = require('fs');
setTimeout(function(){
   Promise.resolve().then(()=>{
       console.log('then2');
   })
},0);
Promise.resolve().then(()=>{
   console.log('then1');
});
fs.readFile('./gitigore',function(){
   process.nextTick(function(){
       console.log('nextTick')
   })
   setImmediate(()=>{
       console.log('setImmediate')
   });
});
複製程式碼

回看我們開頭展示的程式碼,這裡我們的佇列中顯然包含有

setTimeout
Promise.resolve().then
fs.readFile
複製程式碼

這樣的三個主要的任務佇列 依據迴圈階段,我們將程式碼按照迴圈階段的順序展示和執行

// 清空TimerQueue
setTimeout(...)  
// 清空該程式中的微任務
// then1位置的Promise先進入任務佇列
Promise.resolve().then(()=>{ 
   console.log('then1'); // then1
});
Promise.resolve().then(()=>{
    console.log('then2'); // then2
})
// 接著進入IO佇列
fs.readFile(...)
// 優先清空IO佇列的NextTick Queue
process.nextTick(function(){
   console.log('nextTick') // nextTick
})
// 清空micro queue
setImmediate(()=>{
   console.log('setImmediate')//setImmediate
});

複製程式碼

相關文章