---
# System prepended metadata

title: Nodejs
tags: [Nodejs]

---

# Nodejs
###### tags: `Nodejs`

## Nodejs環境的組成
![](https://i.imgur.com/meQE9su.png)
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的執行
![](https://i.imgur.com/iCfp6Jq.png)
眾所皆知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都是在這裡執行的
![](https://i.imgur.com/tQJyUMJ.png)
所謂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會直接在當前階段結束後馬上插隊執行

![](https://i.imgur.com/AC8sdPN.png)

#### 一圈event loop結束後會發生什麼事?
當一圈event loop結束後node會檢查是否還有timer或I/O的事件正在處理，如果有就進下一圈loop處理他們的callback，如果沒有的話就結束程式
![](https://i.imgur.com/iOUfQhK.png)

#### 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
![](https://i.imgur.com/cNQrbvP.png)


##### 小結
Event loop的運作模式有沒有跟公車路線有點像呢，一開始車上載著各種來自不同queue(地區)的callback(乘客)，他們會在各自的對應階段(站牌)執行(下車)

## Event-driven architecture
大部分的node module都是以event-driven為基礎建立的，我們同樣也可以用event-driven為基礎自訂功能
![](https://i.imgur.com/AEUB0SW.png)
### 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的影片你不需要等整部都跑完才能看，而是你邊看他會邊載入後面的內容，這就是串流的應用
![](https://i.imgur.com/oAUlMLD.png)
![](https://i.imgur.com/AYgiCGu.png)


### 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了
![](https://i.imgur.com/VTiSWrR.png)

### 功能
1. 可以直接在ndb畫面中編輯程式碼
2. 設定breakpoint
3. 看scope中的變數
4. app._router.stack會列出所有使用到的middleware
![](https://i.imgur.com/4lJyVVe.png)



