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,處理提供的輸入指令碼,這可能會使非同步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個階段完畢後,進入下一輪的迴圈。
即用一張圖表述為
- 結合程式碼
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
});
複製程式碼