# Nodejs
###### tags: `Nodejs`
## Nodejs環境的組成

node環境是有許多不同的dependency組成的,其中最重要的有兩個,一個是google的V8引擎,一個是libuz
### V8
V8用C++及JS建構的引擎,它能夠將javascript語言轉換成機器語言讓電腦解讀,是使node讀懂javascript的關鍵
### libuv
Libuv是用C語言寫出來的library,它
1. 可以讓node存取系統中的檔案、使用網路等功能
2. 提供了對event loop及非同步I/O(input、output)的支援
3. 提供了Thread pool讓nodejs有能力處理檔案壓縮等對效能有要求的功能
這些功能的結合讓我們可以在node環境中用javascript來做到訪問檔案等複雜的功能,讓javascript不再只是拿來操作網頁元素用的語言
## Nodejs的執行

眾所皆知Javascript是一個單執行緒(single thread)語言,不管有多少人在用都只會有一條路,所以在使用這個語言時須要格外注意非同步的處理,才不會塞車
node環境下執行應用程式時會經過以下步驟
1. 初始化
2. 執行top-level code(只會執行一次,沒有用callback呼叫的code)
3. 引入module
4. 註冊事件
5. 開始event loop
在Event loop中,如果需要處理到較高成本的操作,例如處理檔案、加密、壓縮、查詢DNS等,由於這些動作較為複雜有導致執行緒塞車的可能,node會自動將這些操作丟到Thread pool中(有4~128個Thread)
### Event loop
#### 簡介
Node環境的核心,幾乎所有callback都是在這裡執行的

所謂callback function就是在一個動作結束後會被執行的function
node的application是由callback組成的,這個app會在不同的情況下收到不同的event,這些event就會被放進event loop中做處理,簡單的說就是收到這些event會執行對應的callback,例如收到不同的HTTP request時會做對應的處理(呼叫對應的callback)
Event loop在整個環境中會扮演一個類似指揮官的角色,收到各種事件會命令對應的callback進行處理,收到較複雜的事件則會送到thread pool中進行處理
#### 運作模式
Event loop在事件的處理有一定的順序
event loop中有對應各種事件的階段,而每個階段都有自己的callback queue
常見的四個階段依序為
1. expired timer callbacks
<font color='red'>timer時間到的時候</font>會執行的callbacks(例如setTimeout中的callback),<font color='red'>這邊要特別注意,在這裡執行的callback其實都是以前的event loop中觸發的timer,不會是目前這圈的timer</font>
將該觸發的callbacks觸發完後就會進到下一個階段
2. I/O polling and callbacks
這邊會處理input/output相關的callback,任何跟網路相關的(例如HTTP請求),和處理檔案等動作都會在這階段做完
<font color='red'>大概90%以上的callback都是在這裡處理(因為很多功能其實都是與網路、檔案掛鉤的),是event loop中最繁忙的階段</font>
3. setimmediate callbacks
這個階段會處理第二層的callback可能會呼叫的後續操作
4. close callbacks
關閉server、關閉websocket等事件的callback會在這階段處理,處理完之後到下一圈event loop
除了對應這四個階段的callback queue,還有兩個比較特殊的queue
1. <font color='red'>nextTick queue</font>
被用來處理一些特殊情況,一定要在某階段後馬上執行的callback會放在這
2. <font color='red'>other micro tasks queue</font>
被用來處理promise callback
這兩個queue特殊的點在於他們的callback會直接在當前階段結束後馬上插隊執行

#### 一圈event loop結束後會發生什麼事?
當一圈event loop結束後node會檢查是否還有timer或I/O的事件正在處理,如果有就進下一圈loop處理他們的callback,如果沒有的話就結束程式

#### Event loop測試
現在來寫一些簡單的程式碼來驗證上面對event loop的說明
1. event loop外
```javascript=
const fs = require('fs')
setTimeout(() => console.log('timer1 finished'),0)
setImmediate(() => console.log('immediate1 finished'))
fs.readFile('test-file.txt', () => {
console.log('I/O finished')
})
console.log('top level code')
```
執行結果:
多次執行後發現會有兩種結果
```
// 1
top level code
immediate1 finished
timer1 finished
I/O finished
// 2
top level code
timer1 finished
immediate1 finished
I/O finished
// 若fs讀的是一個空檔案,也有可能排在timer和immediate之前執行,因為不在event loop中
```
這是因為目前的code並不是在event loop中被執行,能確定的順序只有top level code
2. event loop 中
這次將setTimeout和setImmediate放到callback中執行
```javascript=
const fs = require('fs')
setTimeout(() => console.log('timer1 finished'),0)
setImmediate(() => console.log('immediate1 finished'))
fs.readFile('test-file.txt', () => {
console.log('I/O finished')
console.log('-----event loop start-----')
setTimeout(() => console.log('timer2 finished'),0)
setTimeout(() => console.log('timer3 finished'),1000)
setImmediate(() => console.log('immediate2 finished'))
})
console.log('top level code')
```
執行結果
```
top level code
timer1 finished
immediate1 finished
I/O finished
-----event loop start-----
immediate2 finished
timer2 finished
timer3 finished
```
這樣就會是上面在event loop運作模式中的結果了(timer的callback會在下一圈loop的timer expired階段執行)
3. 加上nextTick
```javascript=
fs.readFile('test-file.txt', () => {
console.log('I/O finished')
console.log('-----event loop start-----')
setTimeout(() => console.log('timer2 finished'),0)
setTimeout(() => console.log('timer3 finished'),1000)
setImmediate(() => console.log('immediate2 finished'))
process.nextTick(() => console.log('next tick'))
})
console.log('top level code')
```
執行結果
```
top level code
I/O finished
-----event loop start-----
next tick
immediate2 finished
timer2 finished
timer3 finished
```
會發現next tick會先執行,這跟前面提到的結果一樣,next tick會在各階段結束後馬上執行,這邊的next tick是在timer expired階段後執行的,所以比immediate還早
4. thread pool
這次會在event loop中加入複雜操作(加密)來測試thread pool功能
```javascript=
const fs = require('fs')
const crypto = require('crypto')
const start = Date.now()
fs.readFile('test-file.txt', () => {
console.log('I/O finished')
setTimeout(() => console.log('timer2 finished'),0)
setTimeout(() => console.log('timer3 finished'),1000)
setImmediate(() => console.log('immediate2 finished'))
process.nextTick(() => console.log('next tick'))
crypto.pbkdf2('password', 'salt', 10000, 1024, 'sha512', () => {
console.log(Date.now()-start, 'encrypted')
})
crypto.pbkdf2('password', 'salt', 10000, 1024, 'sha512', () => {
console.log(Date.now()-start, 'encrypted')
})
crypto.pbkdf2('password', 'salt', 10000, 1024, 'sha512', () => {
console.log(Date.now()-start, 'encrypted')
})
crypto.pbkdf2('password', 'salt', 10000, 1024, 'sha512', () => {
console.log(Date.now()-start, 'encrypted')
})
})
```
執行結果
```
I/O finished
next tick
immediate2 finished
timer2 finished
107 encrypted
108 encrypted
118 encrypted
118 encrypted
timer3 finished
```
可以看到四次加密的時間是很接近的
5. 在event loop中使用同步方法
```javascript=
fs.readFile('test-file.txt', () => {
console.log('I/O finished')
setTimeout(() => console.log('timer2 finished'),0)
setTimeout(() => console.log('timer3 finished'),1000)
setImmediate(() => console.log('immediate2 finished'))
process.nextTick(() => console.log('next tick'))
crypto.pbkdf2Sync('password', 'salt', 10000, 1024, 'sha512')
console.log(Date.now()-start, 'encrypted')
crypto.pbkdf2Sync('password', 'salt', 10000, 1024, 'sha512')
console.log(Date.now()-start, 'encrypted')
crypto.pbkdf2Sync('password', 'salt', 10000, 1024, 'sha512')
console.log(Date.now()-start, 'encrypted')
crypto.pbkdf2Sync('password', 'salt', 10000, 1024, 'sha512')
console.log(Date.now()-start, 'encrypted')
})
```
執行結果
```
I/O finished
104 encrypted
191 encrypted
279 encrypted
366 encrypted
next tick
immediate2 finished
timer2 finished
timer3 finished
```
可以看到一個結果,就是同步方法不會被放到event loop中(可以看到同樣4行的執行時間比上面在event loop中的四行慢了很多),如果將同步方法寫在callback中,event loop就會等到這些方法結束後才開始跑(就是塞車啦),這是在開發上需要極力避免的情況
#### Event loop guideline

##### 小結
Event loop的運作模式有沒有跟公車路線有點像呢,一開始車上載著各種來自不同queue(地區)的callback(乘客),他們會在各自的對應階段(站牌)執行(下車)
## Event-driven architecture
大部分的node module都是以event-driven為基礎建立的,我們同樣也可以用event-driven為基礎自訂功能

### observer pattern
用程式碼示範
.on()是listener
.emit()是emitter
這兩個組成了observer pattern
```javascript=
const eventEmitter = require('events')
const myEmitter = new eventEmitter()
myEmitter.on('newSale', () => console.log('sale')) // 對newSale事件進行監聽
myEmitter.on('newSale', goodsCount => console.log(`${goodsCount} goods left`)) // 對newSale事件進行監聽(可以對同一個事件放多個監聽器)
myEmitter.emit('newSale', 10) // 發射newSale事件(可代入參數)
```
執行結果
```
sale
10 goods left
```
### 建立自己的事件
比較好的用法是先建立一個class去繼承 eventEmitter再用該class建立事件
```javascript=
const eventEmitter = require('events')
class myEvent extends eventEmitter {
constructor () {
super() // 創建完成就能使用 eventEmitter class中所有功能
}
}
const myEmitter = new myEvent()
myEmitter.on('newSale', () => console.log('sale'))
myEmitter.on('newSale', goodsCount => console.log(`${goodsCount} goods left`))
myEmitter.emit('newSale', 10)
```
### module中的observer pattern
這邊用http module來舉例
```javascript=
const http = require('http')
const server = http.createServer() // server是基於eventEmitter建立的,所以也能使用on來監聽相關事件
server.on('request', (req, res) => {
console.log('Request received')
res.end('Request received')
})
server.on('request', (req, res) => {
console.log('another request')
})
server.on('close', () => console.log('server close'))
// 啟動server
server.listen(8000, '127.0.0.1', () => {
console.log('listening')
})
```
## Streams
常聽到的串流,意思是可以先解析並使用局部資料
例如youtube的影片你不需要等整部都跑完才能看,而是你邊看他會邊載入後面的內容,這就是串流的應用


### stream應用
先上一個沒使用stream的錯誤示範,這個檔案非常大的時候會導致app卡住,因為他會讀完整個檔案才做動作,在真正的產品上是不能出現這種程式碼的
```javascript=
const fs = require('fs')
const server = require('http').createServer()
server.on('request', (req, res) => {
fs.readFile('test-file.txt', (err, data) =>{
if(err) console.log(err)
res.end(data)
})
})
server.listen(8000,'127.0.0.1', () => console.log('listening'))
```
stream solution
```javascript=
const fs = require('fs')
const server = require('http').createServer()
server.on('request', (req, res) => {
const readable = fs.createReadStream('test-file.txt')
readable.on('data', chunk => {
res.write(chunk)
})
readable.on('end', () => {
res.end()
})
readable.on('error', err => {
console.log(err)
res.statusCode = 500
res.end('not found')
})
})
server.listen(8000,'127.0.0.1', () => console.log('listening'))
```
better stream solution
對上一種方法再次進行改善,上一種方法的readableStream處理網路上的檔案遠比writableStream快,所以可能會因為速度跟不上導致錯誤發生([詳細說明](https://nodejs.org/zh-cn/docs/guides/backpressuring-in-streams/)),由於我們讀的是本地的檔案,所以可能看不太出來與上一種方法的差別,但使用pipe()來處理可以改善讀寫速度相差太大的問題
```javascript=
const fs = require('fs')
const server = require('http').createServer()
const readable = fs.createReadStream('test-file.txt')
readable.on('error', err => {
console.log(err)
res.statusCode = 500
res.end('not found')
})
readable.pipe(res) // readableSource.pip(writableDestination)
})
server.listen(8000,'127.0.0.1', () => console.log('listening'))
```
## Module
## NDB
由google開發的node debug套件
直接安裝在全域使用
```
npm i ndb --global
```
安裝完後到package.json設定npm script
加上這個
```
"debug": "ndb entrypoin.js" // 看你的entry point是哪個檔案
```
之後使用npm run debug指令就能看到這個畫面代表成功開啟ndb了

### 功能
1. 可以直接在ndb畫面中編輯程式碼
2. 設定breakpoint
3. 看scope中的變數
4. app._router.stack會列出所有使用到的middleware
