Node.js是一個構建在Chrome瀏覽器V8引擎
上的JavaScript運行環境, 使用單線程
、事件驅動
、非阻塞I/O
的方式實現了高併發請求,libuv
爲其提供了異步編程的能力。javascript
從這張圖上咱們能夠看出,Node.js底層框架由Node.js標準庫、Node bindings、 底層庫三個部分組成。java
這一層是由Javascript編寫的,也就是咱們使用過程當中直接能調用的API,在源碼中的lib目錄下能夠看到,諸如http、fs、events
等經常使用核心模塊node
這一層能夠理解爲是javascript與C/C++庫之間創建鏈接的橋
, 經過這個橋,底層實現的C/C++庫暴露給javascript環境,同時把js傳入V8
, 解析後交給libuv
發起非阻塞I/O
, 並等待事件循環
調度;linux
這一層主要有如下四塊:編程
順帶看一下libuv的架構圖,可見Nodejs的網絡I/O
、文件I/O
、DNS操做
、還有一些用戶代碼都是在libuv工做的。promise
咱們知道任務調度通常有兩種方案: 一是單線程串行執行
,執行順序與編碼順序一致,最大的問題是沒法充分利用多核CPU,當並行極大的時候,單核CPU理論上計算能力是100%; 另外一種就是多線程並行處理
,優勢是能夠有效利用多核CPU,缺點是建立與切換線程開銷大,還涉及到鎖、狀態同步等問題, CPU常常會等待I/O結束,CPU的性能就白白消耗。瀏覽器
一般爲客戶端鏈接建立一個線程須要消耗2M內存,因此理論上一臺8G的服務器,在Java應用中最多支持的併發數是4000。而Node.js只使用一個線程,當有客戶端鏈接請求時,觸發內部事件,經過非阻塞I/O,事件驅動機制,讓其看起來是並行的。 理論上一臺8G內存的服務器,能夠同時容納3到4萬用戶的鏈接。bash
Node.js採用單線程方案,免去鎖、狀態同步等繁雜問題,又能提升CPU利用率。Node.js高效的除了由於其單線程外,還必須配合下面要說的非阻塞I/O。服務器
首先要清楚,對於一個網絡IO,會涉及到兩個系統對象:網絡
而當一個讀操做發生時,它會經歷兩個階段:
接下來理清這幾個概念:
select/poll
這個function會不斷的輪詢所負責的全部socket,當某個socket有數據到達了,就通知用戶進程。 而epool
經過callback回調通知機制.減小內存開銷,不因併發量大而下降效率,linux下最高效率的I/O事件機制。阻塞I/O,非阻塞I/O,多路複用I/O
都屬於同步I/O。 注意非阻塞I/O在數據從內核拷貝到用戶進程時,進程仍然是阻塞的,因此仍是屬於同步I/O。總結:
阻塞I/O和非阻塞I/O區別在於:在I/O操做的完成或數據的返回前是等待仍是返回(能夠理解成一直等仍是分時間段等) 同步I/O和異步I/O區別在於 :在I/O操做的完成或數據的返回前會不會將進程阻塞(或者說是主動查詢仍是被動等待通知)
因爲Node.js中採用了非阻塞型I/O機制,所以在執行了讀數據的代碼以後,將當即轉而執行其後面的代碼,把讀數據返回結果的處理代碼放在回調函數中,從而提升了程序的執行效率。 當某個I/O執行完畢時,將以事件的形式通知執行I/O操做的線程,線程執行這個事件的回調函數。
執行棧
(execution context stack);事件隊列
(Event queue),當用戶的網絡請求或者其它的異步操做
到來時,會先進入到事件隊列中排隊,並不會當即執行它,代碼也不會被阻塞,繼續往下走,直到主線程代碼執行完畢;事件循環機制
(Event Loop),檢查隊列中是否有要處理的事件,從隊頭取出第一個事件,從線程池
分配一個線程來處理這個事件,而後是第二個,第三個,直到隊列中全部事件都執行完了。 當有事件執行完畢後,會通知主線程,主線程執行回調,並將線程歸還給線程池。這個過程就叫事件循環
(Event Loop);注意:
FIFO(先進先出)執行回調函數的隊列
,一般當事件循環進入到給定階段會執行特定於該階段的全部操做,而後執行該階段隊列的回調事件直到隊列耗盡或者超過最大執行限度爲止,而後事件循環就會走向下一階段;階段概述:
setTimeout
和setInterval
調度的回調。I/O回調函數
。I/O事件
回調,在適當的條件下 node 會阻塞在這個階段。setImmediate
的回調。事件循環的 Pending
、Idle/Prepare
和 Close
階段塗成灰色,由於這些是 Node 在內部使用的階段。
Node.js開發者編寫的代碼僅以微任務形式在主線
、計時器(Timers)
階段、輪詢(Poll)
階段和 查詢(Check)
階段中運行。
總有一種維持 poll 狀態的傾向
;後續 tick 各個階段是否存在不爲空的回調函數隊列
和 最近的計時器時間節點
決定。 若全部隊列爲空且不存在任何計時器,那麼事件循環將 無限制地維持在 poll 階段
;以實現一旦存在 I/O 回調函數加入到 poll 隊列中便可當即獲得執行;poll 階段主要有兩個功能:
process.nextTick() 不在 Event Loop 的任何階段執行,而是在各個階段切換的中間執行
,即從一個階段切換到下個階段前執行。
這裏還須要提一下 macrotask 和 microtask 的概念,macrotask(宏任務)指 Event Loop 每一個階段執行的任務,microtask(微任務)指每一個階段之間執行的任務。
即上述 6 個階段都屬於 macrotask,process.nextTick() 屬於 microtask
。
process.nextTick() 的實現和 v8 的 microtask 並沒有關係,是 Node.js 層面的東西,應該說 process.nextTick() 的行爲接近爲 microtask。 Promise.then 也屬於 microtask 的一種。
能夠經過遞歸 process.nextTick()調用來「餓死」您的 I/O,阻止事件循環到達 輪詢 階段。
promise.then 回調像微處理同樣執行,就像 process.nextTick 同樣。 雖然,若是二者都在同一個微任務隊列中,則將首先執行 process.nextTick 的回調
。 優先級 process.nextTick > promise.then = queueMicrotask
咱們來看這一段代碼在不一樣的環境下執行的結果:
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
複製代碼
首先在瀏覽器環境下,其輸出結果爲:
timer1
promise1
timer2
promise2
複製代碼
藉助於前面關於瀏覽器中Javascript事件循環的知識,不能理解。
而後咱們將其在 Node.js v11.0.0
如下版本執行,獲得結果:
timer1;
timer2;
promise1;
promise2;
複製代碼
但若是是在 Node.js v11.0.0
以上(包括)版本中執行中,將獲得結果:
timer1;
promise1;
timer2;
promise2;
複製代碼
緣由是 node v11 如下只有所有執行了 timers 階段隊列的所有任務才執行微任務隊列,而瀏覽器只要執行了一個宏任務就會執行微任務隊列。 node v11 在 timer 階段的 setTimeout,setInterval 和在 check 階段的 immediate 都在 node v11 裏面都修改成一旦執行一個階段裏的一個任務就馬上執行微任務隊列。 也是爲了和瀏覽器保持一致。
setImmediate
設計爲在當前輪詢 poll 階段完成後執行腳本setTimeout
計劃在以毫秒爲單位的最小閾值過去以後運行腳本不在 I/O 回調(即主模塊)
內的腳本,則兩個計時器的執行順序是不肯定的,由於它受機器性能的約束,好比:
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
複製代碼
輸出順序是不肯定的。
咱們知道 setTimeout 的回調函數在 timer 階段執行,setImmediate 的回調函數在 check 階段執行,Event loop 的開始會先檢查 timer 階段,可是在開始以前到 timer 階段會消耗必定時間,因此就會出現兩種狀況:
而若是這兩個調用在一個I/O回調中,那麼immediate老是先執行。
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
複製代碼
分析以下:
setInterval(() => {
console.log('setInterval')
}, 100)
process.nextTick(function tick () {
process.nextTick(tick)
})
複製代碼
運行結果:setInterval 永遠不會打印出來。
process.nextTick 會無限循環,將 Event loop 阻塞在 microtask 階段,致使 Event loop 上其餘 macrotask 階段的回調函數沒有機會執行。 解決方法一般是用 setImmediate 替代 process.nextTick,以下:
setInterval(() => {
console.log('setInterval')
}, 100)
setImmediate(function immediate () {
setImmediate(immediate)
})
複製代碼
運行結果:每 100ms 打印一次 setInterval。
process.nextTick 內執行 process.nextTick 仍然將 tick 函數註冊到當前 microtask 的尾部,因此致使 microtask 永遠執行不完; setImmediate 內執行 setImmediate 會將 immediate 函數註冊到下一次 Event loop 的 check 階段,而不是當前正在執行的 check 階段,因此給了 Event loop 上其餘 macrotask 執行的機會。
setImmediate(() => {
console.log('setImmediate1')
setImmediate(() => {
console.log('setImmediate2')
})
process.nextTick(() => {
console.log('nextTick')
})
})
setImmediate(() => {
console.log('setImmediate3')
})
複製代碼
運行結果在 node v11如下是:
setImmediate1
setImmediate3
nextTick
setImmediate2
複製代碼
在node v11以上是:
setImmediate1
nextTick
setImmediate3
setImmediate2
複製代碼
緣由同案例一
setImmediate(() => {
console.log(1)
setTimeout(() => {
console.log(2)
}, 100)
setImmediate(() => {
console.log(3)
})
process.nextTick(() => {
console.log(4)
})
})
process.nextTick(() => {
console.log(5)
setTimeout(() => {
console.log(6)
}, 100)
setImmediate(() => {
console.log(7)
})
process.nextTick(() => {
console.log(8)
})
})
console.log(9)
複製代碼
運行結果在 node v11如下是:
9
5
8
1
7
4
3
6
2
複製代碼
在 node v11以上是:
9
5
8
1
4
7
3
6
2
複製代碼
緣由請自行分析。