[TOC]
# 核心
nodejs的核心特性是異步 I/O,事件驅動;
# 同步I/O 和 異步I/O
那么同步 I/O 和異步 I/O 又有什么區別么?是不是只要做到非阻塞 IO 就可以實現異步I/O呢?
其實不然。
* 同步 I/O(synchronous I/O)做I/O operation 的時候會將 process 阻塞,所以阻塞 I/O,非阻塞I/O,IO 多路復用I/O都是同步I/O。
* 異步I/O(asynchronous I/O)做I/O opertaion的時候將不會造成任何的阻塞。
非阻塞 I/O 都不阻塞了為什么不是異步 I/O 呢?其實當非阻塞 I/O 準備好數據以后還是要阻塞住進程去內核拿數據的,所以算不上異步I/O。

# 單線程
在 Java、PHP 或者 .net 等服務器端語言中,會為每一個客戶端連接創建一個新的線程。而每個線程需要耗費大約2MB內存。也就是說,理論上,一個8GB 內存的服務器可以同時連接的最大用戶數為4000個左右。要讓Web應用程序支持更多的用戶,就需要增加服務器的數量,而 Web 應用程序的硬件成本當然就上升了。
Node.js 不為每個客戶連接創建一個新的線程,而僅僅使用一個線程。當有用戶連接了,就觸發一個內部事件,通過非阻塞`I/O、事件驅動機制`,讓 Node.js 程序宏觀上也是并行的。使用 Node.js ,一個8GB內存的服務器,可以同時處理超過4萬用戶的連接。
另外,單線程帶來的好處,操作系統完全不再有線程創建、銷毀的時間開銷。但是單線程也有很多弊端,會在 Node.js 的弊端詳細講解,請繼續看。
Node.js 對 http 服務的模型:

Node.js的單線程 指的是**主線程是“單線程”**,由主要線程去按照編碼順序一步步執行程序代碼,假如遇到同步代碼阻塞,主線程被占用,后續的程序代碼執行就會被卡住。實踐一個測試代碼:
```
var?http?=?require('http');function?sleep(time)?{?
var?_exit?=?Date.now()?+?time?*?1000;?
while(?Date.now()?<?_exit?)?{}????
return?;
}
var?server?=?http.createServer(function(req,?res){
????sleep(10);
????res.end('server?sleep?10s');
});
server.listen(8080);
```
下面為代碼塊的堆棧圖:

先將`index.js`的代碼改成這樣,然后打開瀏覽器,你會發現瀏覽器在10秒之后才做出反應,打出`Hello Node.js`。
**JavaScript是解析性語言,代碼按照編碼順序一行一行被壓進 stack 里面執行,執行完成后移除然后繼續壓下一行代碼塊進去執行。**
上面代碼塊的堆棧圖,當主線程接受了 request 后,程序被壓進同步執行的 sleep 執行塊(我們假設這里就是程序的業務處理),如果在這 10s 內有第二個request進來就會被壓進stack里面等待 10s 執行完成后再進一步處理下一個請求,后面的請求都會被掛起等待前面的同步執行完成后再執行。
那么我們會疑問:為什么一個單線程的效率可以這么高,同時處理數萬級的并發而不會造成阻塞呢?就是我們下面所說的 -------- **事件循環機制**。
# EventLoop(事件循環機制)
根據 Node.js官方介紹,每次事件循環都包含了**6個階段(Phase)**,對應到 libuv 源碼中的實現,如下圖所示:

*每個框框里每一步都是事件循環機制的一個階段。*
EventLoop 的每一次循環都需要依次經過上述的階段。
~~~shell
| nextTick(隊列執行)
│ ┌──────────┴────────────┐
│ │ timers │
│ └──────────┬────────────┘
| nextTick(隊列執行)
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
| nextTick(隊列執行)
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘
| nextTick(隊列執行) ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ | |
| nextTick(隊列執行) │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
| nextTick(隊列執行)
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
1. 在進入事件循環的每個階段前(timers queue, IO events queue, immediates queue, close handlers queue),Node 會檢查`nextTick`queue . 如果 queue 不為空, Node 會執行`nextTick`queue 中的任務,直至隊列為空,然后才會進入下一階段。
2. 每個階段都有自己的 callback 隊列,所以進入某個階段后,會從所屬的隊列中取出 callback 來執行,**當隊列為空或者被執行 callback 的數量達到系統的最大數量時,進入下一階段**。
~~~
這六個階段都執行完畢稱為一輪循環。
> 參考:[Node事件循環系列——2、Timer 、Immediate 和 nextTick](https://zhuanlan.zhihu.com/p/87579819)
## 階段概覽
* **timers(定時器)** : 此階段執行那些由 `setTimeout()` 和 `setInterval()` 調度的回調函數;
當使用 setTimeout 或者 setInterval 指定延遲的時間到達之后,會將任務添加到 timers 隊列中,等待其他階段的任務隊列中的執行完成之后執行。**所以實際腳本執行的時刻 >= 設定的時間,因為此時可能還有其他的任務在隊列中等待執行**
* **I/O callbacks(I/O回調)** : 此階段會執行幾乎所有的回調函數, 除了 **close callbacks(關閉回調)** 和 那些由 **timers** 與 `setImmediate()` 調度的回調;
* **idle(空轉), prepare** : 此階段僅 node 內部使用;與我們編程關系不大。
* **poll(輪詢)** : 檢索新的 I/O 事件; 在恰當的時候 Node 會阻塞在這個階段
poll 是一個至關重要的階段,這一階段中,系統會做兩件事情
* 回到 timer 階段執行回調
* 執行 I/O 回調
并且在進入該階段時如果沒有設定的 timer 的話,會發生以下兩件事情
* 如果 poll 隊列不為空,會遍歷回調隊列并同步執行,直到隊列為空或者達到系統限制
* 如果 poll 隊列為空時,會有兩件事發生
* 如果有 `setImmediate` 回調需要執行,poll 階段會停止并且進入到 check 階段執行回調
* 如果沒有 `setImmediate` 回調需要執行,會等待回調被加入到隊列中并立即執行回調,這里同樣會有個超時時間設置防止一直等待下去
當然設定了 timer 的話且 poll 隊列為空,則會判斷是否有 timer 超時,如果有的話會繞回到 timer 階段執行回調。
* **check(檢查)** : 此階段只處理 `setImmediate()` 設置的回調;因為 Poll 階段可能設置一些回調, 希望在 Poll 階段后運行. 所以在 Poll 階段后面增加了這個 Check 階段。
* **close callbacks(關閉事件的回調)**: 諸如 `socket.on('close', ...)` 此類的回調在此階段被調用;用于資源清理。
每個階段都有一個 **FIFO 隊列**來**執行回調**。雖然每個階段都是特殊的,但通常情況下,當事件循環進入給定的階段時,它將執行特定于該階段的任何操作,然后在該階段的隊列中執行回調,直到隊列用盡或**最大回調數**已執行。當該隊列已用盡或達到回調限制,事件循環將移動到下一階段,等等。
那么我們平常的異步 io 是在哪個階段執行的呢,答案是 poll 階段。
實例:
```
const fs = require('fs');
function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
}, 100);
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});
```
假定 `fs.readFile` 需要95毫秒完成,執行回調函數需要10ms,setTimeout 設定為 100ms 后執行。
那么程序的執行順序如下:
1. [0-95]ms 事件隊列為空,nodejs 保持等待
2. [95-105]ms 執行 readFile 的回調
3. 105ms 執行 setTimeout 的回調
4. 在100ms時,setTimeout 到達指定的事件閾值,被放入 timer 階段的隊列中,等待pool 階段的任務(這里是readFile)結束了之后再被調用。
## nextTickQueue & microTaskQueue

對于日常開發來說,我們比較關注的是 timers、I/O callbacks、check 階段。
node 和瀏覽器相比一個明顯的不同就是**node 在每個階段結束后都會去執行所有 microtask 任務**。
對于這個特點,可以做個試驗:
```
console.log('main');
setImmediate(function() {
console.log('setImmediate');
});
new Promise(function(resolve, reject) {
resolve();
}).then(function() {
console.log('promise.then');
});
```
代碼的執行結果是:
```
main
promise.then
setImemediate
```
在 node 事件循環的每一個子階段退出之前都會按順序執行如下過程:
* ....
* 檢查是否有 `process.nextTick` 回調,如果有,全部執行。
* 檢查是否有 `microtaks`,如果有,全部執行。
* 退出當前階段。
# `process.nextTick` 和 `setImmediate`
`process.nextTick` 并不是事件循環中的一部分,指定的回調函數將會被加入到 **nextTickQueue 隊列**中。
nextTickQueue 將在完成當前階段迭代之后和開始下一次事件循環迭代之前處理完所有回調。具體來說,就是在一段代碼執行時候,會先檢查 nextTickQueue,如果 nextTickQueue 中有回調,會先**執行完 nextTickQueue 中的所有回調**,而不管現在是在事件循環的哪一個階段。
通過遞歸使用 `process.nextTick` 可以阻止事件循環。
相對于瀏覽器環境,node 環境下多出了 `setImmediate` 和 `process.nextTick` 這兩種異步操作。
`setImmediate` 的回調函數是被放在 check 階段執行,即相當于事件循環的最后階段了。而 `process.nextTick` 會被當做一種 **microtask**,前面提到每個階段結束后都會執行所有 microtask 任務,所以 `process.nextTick` 有種類似于插隊的作用,可以趕在下個階段前執行,但它和 `promise.then` 哪個先執行呢?通過一段代碼來實驗:
```
console.log('main');
process.nextTick(function() {
console.log(‘nextTick’)
})
new Promise(function(resolve, reject) {
resolve();
}).then(function() {
console.log('promise.then');
});
```
代碼的執行結果是:
```
main
nextTick
promise.then
```
事實證明,**`process.nextTick` 的優先級會比 `promise.then` 高**。
## `process.nextTick` 的饑餓陷阱
`process.nextTick`的優勢在于它能夠插入到每個階段之后,在當前階段執行完畢后就能立馬執行。然而它的這個優點也導致了如果調用不當就容易陷入饑餓陷阱。具體就是當遞歸地調用 `process.nextTick` 的時候,事件循環一直無法進入到下一個階段,導致了后面階段的事件一直無法被執行,產生饑餓問題。
看一個例子就很容易明白
```
let i = 0;
setImmediate(function() {
console.log('setImmediate');
});
function callback() {
console.log(‘nextTick’ + i++);
if (i < 1000) {
process.nextTick(callback);
}
}
callback();
```
執行的結果是: `nextTick0 nextTick1 nextTick2 … nextTick999 setImmediate`
`setImmediate` 的回調會一直等待到 `process.nextTick` 任務都完成后才能被執行。
## 小結
1. node 的事件循環機制和瀏覽器的有所不同,多出了 `setImmediate` 和 `process.nextTick` 這兩種異步方式。由于`process.nextTick` 會導致 **I/O 饑餓**,所以官方也推薦使用 `setImmediate`。
2. node 雖然是單線程的設計,但它也能實現高并發。原因在于它的主線程事件循環機制和底層線程池的實現。
3. 這種機制決定了 node 比較適合 I/O 密集型應用,而不適合 CPU 密集型應用。
4. `process.nextTick()` 是 node 早期版本無 `setImmediate` 時的產物,node 作者推薦我們盡量使用 `setImmediate`。
> 官網:We recommend developers use`setImmediate()`in all cases…
# EventEmitter 和事件循環的關系
EventEmitter 實現了發布訂閱模式,但是 EventEmitter 回調函數的執行本身不是異步的,當 `event.emit(‘event’)` 執行的時候,**所有訂閱了event事件的回調函數會立即執行(按序)**。但是我們監聽 tcp 連接的時候,連接的回調函數時按順序執行的,前面的連接會阻塞后面的響應,這是因為使用了 `process.nextTick` 或者 `setImmediate`。
比如:
```
http.on('request', function(req,res){
...
})
```
如果 request被 `while` 循環阻塞,那么后面的 http 請求都會在 pending 狀態,因為此時他們正在隊列中。
下面是另一個例子:
```
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
process.nextTick(() => {
this.emit('event');
})
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
```
上面將 `this.emit(‘event’)` 放入 nextTickQueue 隊列,所以注冊 event 事件的代碼會先執行,這樣 event 事件被 `emit` 的時候,事件是被注冊了的。
# 線程
Node.js是單線程的,除了系統 I/O 之外,在它的事件輪詢過程中,同一時間只會處理一個事件。你可以把事件輪詢想象成一個大的隊列,在每個**時間點**上,系統只會處理一個事件。即使你的電腦有多個 CPU 核心,你也無法同時并行的處理多個事件。但也就是這種特性使得node.js適合處理 I/O 型的應用,不適合那種 CPU 運算型的應用。在每個 I/O 型的應用中,你只需要給每一個輸入輸出定義一個回調函數即可,他們會自動加入到事件輪詢的處理隊列里。當 I/O 操作完成后,這個回調函數會被觸發。然后系統會繼續處理其他的請求。
node.js單線程只是一個js主線程,本質上的異步操作還是由線程池完成的,node 將所有的阻塞操作都交給了內部的線程池去實現,本身只負責不斷的往返調度,并沒有進行真正的 I/O 操作,從而實現異步非阻塞 I/O,這便是node單線程和事件驅動的精髓之處了。
**cpu核數與線程之間的關系**
在過去單CPU時代,單任務在一個時間點只能執行單一程序。之后發展到多任務階段,計算機能在同一時間點并行執行多任務或多進程。雖然并不是真正意義上的“同一時間點”,而是多個任務或進程共享一個CPU,并交由操作系統來完成多任務間對CPU的運行切換,以使得每個任務都有機會獲得一定的時間片運行。而現在多核CPU的情況下,同一時間點可以執行多個任務,具體到這個任務在CPU哪個核上運行跟操作系統和 CPU 本身的設計相關
## 線程驅動和事件驅動
* **線程驅動**就是當收到一個請求的時候,將會為該請求開一個新的線程來處理請求。一般存在一個線程池,線程池中有空閑的線程,會從線程池中拿取線程來進行處理,如果線程池中沒有空閑的線程,新來的請求將會進入隊列排隊,直到線程池中空閑線程。
* **事件驅動**就是當進來一個新的請求的時,請求將會被壓入隊列中,然后通過一個循環來檢測隊列中的事件狀態變化,如果檢測到有狀態變化的事件,那么就執行該事件對應的處理代碼,一般都是回調函數。
對于事件驅動編程來說,**如果某個時間的回調函數是計算密集型,或者是阻塞I/O,那么這個回調函數將會阻塞后面所有事件回調函數的執行**。這一點尤為重要
。
示例:
```
var fs = require("fs");
var debug = require('debug')('example4');
debug("begin");
setTimeout(function(){
debug("timeout1");
/**
* 模擬計算密集
*/
for(var i = 0 ; i < 1000000 ; ++i){
for(var j = 0 ; j < 100000 ; ++j);
}
});
setTimeout(function(){
debug("timeout2");
});
debug('end');
/**
Sat, 21 May 2016 08:53:27 GMT example4 begin
Sat, 21 May 2016 08:53:27 GMT example4 end
Sat, 21 May 2016 08:53:27 GMT example4 timeout1
Sat, 21 May 2016 08:54:09 GMT example4 timeout2 // 注意這里的時間晚了好久
*/
```
# 新版本 v11 的 Timers 和 Microtasks
Node v11中的新更改與瀏覽器行為相匹配,從而提高了 瀏覽器的 JavaScript 在Node.js 中 的可重用性。 但是,這一重大變化可能會破壞明確依賴舊行為的現有 Node.js 應用程序。
因此,如果要升級到 Node v11或更高版本(最好是下一個 LTS v12),您需要認真的注意一下。
> [又被node的eventloop坑了,這次是node的鍋](https://juejin.im/post/5c3e8d90f265da614274218a)
示例:
```
let racer1 = function() {
setTimeout(() => {
console.log("timeout1")
Promise.resolve().then(() => console.log('promise resolve1'));
process.nextTick(() => console.log('next tick1'))
}, 0);
setImmediate(() => console.log("immediate1"));
process.nextTick(() => console.log("nextTick1"));
}
let racer2 = function() {
process.nextTick(() => console.log("nextTick2"));
setTimeout(() => {
console.log("timeout2")
Promise.resolve().then(() => console.log('promise resolve2'));
process.nextTick(() => console.log('next tick2'))
}, 0);
setImmediate(() => console.log("immediate2"));
}
let racer3 = function() {
setImmediate(() => console.log("immediate3"));
process.nextTick(() => console.log("nextTick3"));
setTimeout(() => console.log("timeout3"), 0);
}
racer1()
racer2()
racer3()
```
node v10 運行結果:
```
nextTick1
nextTick2
nextTick3
timeout1
timeout2
timeout3
next tick1
next tick2
promise resolve1
promise resolve2
immediate1
immediate2
immediate3
```
node v11 運行結果:
```
nextTick1
nextTick2
nextTick3
timeout1
next tick1
promise resolve1
timeout2
next tick2
promise resolve2
timeout3
immediate1
immediate2
immediate3
```
# 工作流示例
## `setTimeout`和`setImmediate`
```
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
```
上述二者的執行順序是不確定的。
因為在 node 中,`setTimeout(cb, 0) === setTimeout(cb, 1);` 而 `setImmediate` 屬于 uv_run_check 的部分,確實每次 loop進來,都是先檢查 uv_run_timer 的,但是由于 cpu 工作耗費時間,比如第一次獲取的 hrtime 為 0 那么 `setTimeout(cb, 1)` ,超時時間就是 loop->time = 1(ms,node 定時器精確到 1ms,但是 hrtime 是精確到納秒級別的)所以第一次loop進來的時候就有兩種情況:
> 1.由于第一次 loop 前的準備耗時超過 1ms,當前的 loop->time >=1 ,則 `uv_run_timer` 生效,timeout 先執行
> 2.由于第一次 loop 前的準備耗時小于 1ms,當前的 loop->time < 1,則本次loop中的第一次 `uv_run_timer` 不生效,那么 `io_poll` 后先執行 `uv_run_check` ,即 immediate 先執行,然后等 `close cb` 執行完后,繼續執行 `uv_run_timer`
但當二者在異步 i/o callback 內部調用時,總是先執行 `setImmediate`,再執行 `setTimeout`
```
var fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
```
因為 `fs.readFile` callback 執行完后,程序設定了 timer 和 `setImmediate`,因此 poll 階段不會被阻塞進而進入 check 階段先執行 `setImmediate`,最后close callbacks 階段結束后檢查 timer,執行 `timeout` 事件。
## `process.nextTick`
```
var fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
process.nextTick(()=>{
console.log('nextTick3');
})
});
process.nextTick(()=>{ console.log('nextTick1'); })
process.nextTick(()=>{ console.log('nextTick2'); })
});
// 輸出:
nextTick1
nextTick2
setImmediate
nextTick3
setTimeout
```
1. 從 poll —> check 階段,先執行 `process.nextTick`,nextTick1,nextTick2。
2. 然后進入 check 執行 `setImmediate`,`setImmediate` 執行完后,出 check,進入close callback 前,執行 `process.nextTick`,nextTick3。
3. 最后進入 timer 執行 `setTimeout
`
# 參考
[瀏覽器與Node的事件循環(Event Loop)有何區別?](https://cnodejs.org/topic/5c3d554fa4d44449266b1077)
[一次弄懂Event Loop(徹底解決此類面試問題)](https://zhuanlan.zhihu.com/p/55511602)
[nodejs 異步I/O和事件驅動](https://blog.csdn.net/ii1245712564/article/details/51473803)
[Node.js event loop workflow & lifecycle in low level](http://voidcanvas.com/nodejs-event-loop/)
[Node.js: How even quick async functions can block the Event-Loop, starve I/O](https://snyk.io/blog/nodejs-how-even-quick-async-functions-can-block-the-event-loop-starve-io/)
[Node.js event loop architecture](https://medium.com/preezma/node-js-event-loop-architecture-go-deeper-node-core-c96b4cec7aa4)
[Nodejs:單線程為什么能支持高并發?](https://www.cnblogs.com/linzhanfly/p/9082895.html)
[Event Loop and the Big Picture — NodeJS Event Loop Part 1](https://blog.insiderattack.net/event-loop-and-the-big-picture-nodejs-event-loop-part-1-1cb67a182810)
[深入了解nodejs的事件循環機制](https://blog.csdn.net/li420520/article/details/82900716)
[How Node Event Loop REALLY Works: Or Why Most of the Event Loop Diagrams are WRONG](https://webapplog.com/event-loop/)