# 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)