>[info]部分內容來源:《新時期的Node.js入門》- 李鍇
推薦閱讀:
[https://www.zhihu.com/question/327657434/answer/715340900](https://www.zhihu.com/question/327657434/answer/715340900)
[https://elemefe.github.io/node-interview/#/sections/zh-cn/](https://elemefe.github.io/node-interview/#/sections/zh-cn/)
[TOC]
# 底層機制
## 單線程與多線程
其他語言(如 Java、C++等)都有多線程的語言特性,即開發者可以派生出多個線程來協同工作;Node 并沒有提供多線程的機制,開發者無法在一個獨立進程中增加新的線程,但是可以派生出多個進程來達到并行完成工作的目的。
Node 的底層實現(C++)并非是單線程的,libuv 會通過類似線程池的實現來模擬不同操作系統下的異步調用,這對開發者來說是不可見的。
<span style="color: MediumOrchid;font-size: 20px;">Libuv 中的多線程</span>

Libuv 是一個跨平臺的異步 IO 庫,它結合了 UNIX 下的 libev 和 Windows下的 IOCP 的特性,最早由 Node 的作者開發,專門為 Node 提供多平臺下的異步 IO 支持。Node 中的非阻塞 IO 以及事件循環的底層機制,都是由 libuv 來實現的。
在 Windows 環境下,libuv直接使用 Windows 的 IOCP(I/O Completion Port)來實現異步 IO。在非 Windows 環境下,libuv 使用多線程來模擬異步 IO。
以 readFile 為例,讀取文件的系統調用是由 libuv 來完成的,Node 只負責調用 libuv 的接口,等數據返回后再執行對應的回調方法。
## 并行與并發
首先搞清楚這兩個概念的差別:這里以一個排隊取火車票的場景來進行介紹
并發(Concurrent):并發是假設有兩個隊伍但只有 1 個取票機,兩個隊伍輪流取票。
并行(Parallel):并行是假設有兩個隊伍,不同的是開放了 2 個取票機,那么這兩個隊列可以同時向前移動。
>以操作系統的角度:并發是指宏觀上在一段時間內能同時運行多個程序,而并行則指同一時刻能運行多個指令。并行需要硬件支持,如多流水線、多核處理器或者分布式計算系統。操作系統通過引入進程和線程,使得程序能夠并發運行。
Node 中的并發:單線程支持高并發,通常是依靠異步+事件循環來實現的,異步使得代碼能在面臨多個請求時不會發生阻塞,事件循環提供了 IO 調用結束后調用回調函數的能力。
>[success]多個請求同時到達時,Java、C++ 等能通過開多個線程來處理請求,而 node.js 卻不能開多線程,其利用事件驅動和異步 I/O 的特性極大地提升了程序性能從而使其有能力這種高并發場景。具體的解釋可以參考《深入淺出 node.js》筆記部分。
## Node中的事件循環(event loop)
瀏覽器的事件循環中提到了宏任務和微任務,在 node.js 中,事件循環被分成了 6 個不同的階段。**每個階段執行特定的宏任務,每執行完一個階段的回調,就清空一次微任務隊列。**
宏任務包括?`script?, setTimeout?, setInterval?, setImmediate?, I/O?, UI rendering`
微任務包括`?process.nextTick, promise.then, Object.observe?, MutationObserver`
[MutationObserver使用方式見這里](http://javascript.ruanyifeng.com/dom/mutationobserver.html)
*****

* timers: 用于處理 setTimeout() 和 setInterval() 的回調。
* I/O callbacks: 大多數的回調方法會在這個階段執行,除了 timers、close 和 setImmediate
* idle, prepare: 僅內部使用。
* poll: 輪訓,不斷檢查有沒有新的 I/O 事件
* check: 執行 setImmediate() 事件的回調。
* close callbacks: 處理一些 close 相關的事件,例如 socket.on('close',...)
<span style="color: MediumOrchid;font-size: 20px;">process.nextTick 與 setImmediate</span>
`setImmediate`接受一個回調函數作為參數,其不像 setTimeout 一樣可以設置定時器的時間,事件循環的整個 check 階段是為 setImmediate 方法而設置的:一般情況下,當事件循環到達 poll 階段后,就會檢查當前代碼是否調用了 setImmediate,如果一個回調函數是被 setImmediate 方法調用的,事件循環就會跳出 poll 階段而進入 check 階段。
`process.nextTick`接受一個回調函數作為參數,該方法定義的回調方法會被加入到名為 nextTickQueue 的隊列中,在事件循環的任何階段,如果 nextTickQueue 不為空,都會在當前階段操作結束后優先執行 nextTickQueue 中的回調函數。
>網上有的文章說 process.nextTick 是 idle 觀察者,setImmediate 屬于 check 觀察者(《深入淺出 node.js 》里也是這么說的,不過那也是好幾年前了),這個實際上與 node 的版本有關,一般也不用特別糾結。可以閱讀 [這篇帖子](https://segmentfault.com/q/1010000011914016/a-1020000011915491)
>[warning]看上去似乎和微任務的執行是類似的,那么 promise.then 和 process.nextTick 的回調誰更先執行呢?setImmediate 和 nextTick 的出現是為了解決什么問題呢?
```js
let promise = new Promise((resolve, reject) => {
resolve()
})
promise.then(() => {
console.log('promise')
})
process.nextTick(function () {
console.log('nextTick')
})
// 打印順序:nextTick promise 無論 Promise 放在之前還是之后都是一樣
```
看上去似乎是 nextTick 的回調比 promise.then 的回調優先級更高
*****
TODO
- node 事件循環案例
- node 事件循環補充要點
*****
# 常用模塊(原生node.js)
## Buffer
Buffer 是 node.js 特有的(區別于瀏覽器 JavaScript)的數據類型,主要用來處理二進制數據。前后端通信時二進制數據流十分常見(例如傳輸一張 gif 圖片)。Buffer 屬于固有(built-in)類型,因此無需 require 引入。
在文件操作和網絡操作中,如果不顯式聲明編碼格式,其返回數據的默認類型就是 Buffer。
**拼接Buffer**
```
var data = []
res.on('data', function (chunk) {
data.push(chunk)
})
res.on('end', function () {
var buf = Buffer.concat(data)
console.log(buf.toString())
})
```
更多與 Buffer 相關的內容見《深入淺出 node.js》部分
## 文件系統(File System)
該模塊提供了讀寫文件的能力,借助于底層的 linux 的 C++ API 來實現,這些 API 大多提供同步和異步兩種版本。下面僅介紹幾個常用的。
- `fs.readFile(file[, options], callback)`
readFile 方法用于異步讀取文本文件中的內容,適用于體積較小的文件,數百 MB 的文件建議使用 stream。readFile 讀出的數據需要在回調方法中獲取,而 readFileSync 直接返回文本數據內容
```js
const fs = require('fs')
fs.readFile('foo.txt', function (err, data) {
if (err) throw err
console.log(data)
})
// 如果不指定readFile的coding配置,其會返回Buffer格式
// <Buffer 48 65 .. .. ..>
const data = fs.readFileSync('foo.txt', {encoding: 'UTF-8'})
```
- `fs.writeFile(file, data[, options], callback)`
fs.writeFile('文件路徑','要寫入的內容',['編碼'],'回調函數')
寫入時如果文件不存在,則會嘗試創建它,默認的 flag 為 ’w’,r 代表讀取文件,w 代表寫文件,a 代表追加。
[https://www.cnblogs.com/starof/p/5038300.html](https://www.cnblogs.com/starof/p/5038300.html)
```js
fs.writeFile(filePath, mp3, 'binary', err => {
if (err) {
res.json({
error: 1,
msg: '下載失敗'
})
} else {
res.json({
error: 0,
msg: '下載成功',
path: downloadUrl
})
}
})
```
- `fs.stat(path, callback)`
stat 方法通常用于獲取文件的狀態,通常開發者可以在調用 open()、read()、或者 write 方法之前調用 fs.stat 方法,用來判斷文件是否存在
如果文件存在,則返回文件的狀態信息,如下
```shell
Stats {
dev: 215266619,
mode: 33206,
nlink: 1,
uid: 0,
gid: 0,
rdev: 0,
blksize: undefined,
ino: 2251799814181389,
size: 2023,
blocks: undefined,
atimeMs: 1557839146143.9646,
mtimeMs: 1559027178498.3296,
ctimeMs: 1559027178498.3296,
birthtimeMs: 1557839146143.9646,
atime: 2019-05-14T13:05:46.144Z,
mtime: 2019-05-28T07:06:18.498Z,
ctime: 2019-05-28T07:06:18.498Z,
birthtime: 2019-05-14T13:05:46.144Z }
```
如果文件不存在,則會出現 `Error: ENOENT: no such file or directory` 的錯誤
**實例:獲取目錄下所有的文件名**
這是一個常見的需求,實現這個功能只需要 fs.readdir 以及 fs.stat 兩個 API,readdir 用于獲取目錄下的所有文件或者子目錄,stat 用于判斷具體每條記錄時文件還是子目錄,如果是子目錄,則遞歸調用整個方法
```
const fs = require('fs')
function getAllFileFromPath(path) {
fs.readdir(path, function (err, res) {
for (let subPath of res) {
// 這里使用同步方法而不是異步
let statObj = fs.statSync(path + '/' + subPath)
if (statObj.isDirectory()) { // 判斷是否為目錄
console.log('Dir: ', subPath)
// 如果是文件夾,遞歸獲取子目錄中的文件列表
} else {
console.log('File: ', subPath)
}
}
})
}
getAllFileFromPath(__dirname)
```
## HTTP 服務
HTTP 模塊提供了一系列用于網絡傳輸的 API,這些 API 大都位于比較底層的位置,可以讓開發者自由控制整個 HTTP 傳輸過程
在 HTTP 模塊中,node 定義了一些頂級的類、屬性以及方法,如下所示
*****
Class: http.Agent
Class: http.ClientRequest
Class: http.Server
Class: http.ServerResponse
Class: http.IncomingMessage
http.METHODS
http.STATUS_CODES
http.createClient([port], [, host])
http.createServer([requestListener])
http.get(options[, callback])
http.request(options[, callback])
......
*****
### 創建 HTTP 服務
```
const http = require('http')
const server = http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'})
res.end('Hello World!')
})
server.listen(3000)
```
上面的代碼使用 createServer 方法創建了一個簡單的 HTTP 服務器,該方法返回一個 http.server 類的實例,createServer 方法包含了一個匿名的回調函數,該函數有兩個參數 req 和 res,它們分別是 IncomingMessage 和 ServerResponse 的實例。分別表示 HTTP 的 request 和 response 對象,服務器創建完成后,node 進程開始循環監聽 3000 端口(由listen方法實現)
### 處理 HTTP 請求
**request 對象**
*****
**1.method、URL**
```
const method = req.method
const url = req.url
http://example.com/index.html?nam=Lear
-> url: /index.html?name=Lear
```
**2.header**
http header 通常為以下的形式
```
{
'content-length': '123',
'content-type': 'text/plain',
'connection': 'keep-alive',
'host': 'mysite.com'
...
}
```
通過 req.headers 獲取一個 JSON 對象,可以對屬性名進行單獨索引
```
const headers = req.headers
const userAgent = headers['user-agent']
```
**3.request body**
node 通過 stream 來處理 HTTP 的請求體,stream 注冊了 data 和 end 兩個事件,可以用如下的代碼來獲取完整的 HTTP 請求體
```
let body = []
request.on('data', chunk => {
body.push(chunk)
}).on('end', () => {
body = Buffer.concat(body).toString()
})
```
**response對象**
*****
**1.設置 response header**
通過`setHeader`方法可以設置 response 的頭部信息
```js
response.setHeader('Content-Type': 'application/json')
```
`setHeader` 方法只能設置 response header 單個屬性的內容,如果想要一次性設置所有的響應頭和狀態碼,可以使用`writeHead`方法
`writeHead` 方法用于定義 HTTP 響應頭,包括狀態碼等一系列屬性
```js
response.writeHead(200, {
'Content-Length': Buffer.byteLength(body),
'Content-Type': 'text/plain'
})
```
調用該方法后,服務端向客戶端發送 HTTP 響應頭,后面通常會跟著調用 `res.write` 等方法,響應頭不可重復發送;有時開發者并不會顯式調用該方法,調用 `res.end` 方法也會調用`writeHead`方法,此時 statusCode 會自動設置為 200
**2.response body**
response 對象是一個 writableStream 實例,可以直接調用 write 方法進行寫入,寫入完成后,再調用 end 方法將該 stream 發送到客戶端
```js
response.write('<html>')
response.write('<body>')
response.write('<h1>Hello World!</h1>')
response.write('</body>')
response.write('</htnl>')
response.end()
// 或者寫成這樣:直接將 response body 作為 end 方法的參數返回
response.end('<html><body><h1>Hello World!</h1></body></html>')
```
**3.response.end**
end 方法在每個 HTTP 請求的最后都會被調用,開發者應該調用該方法來結束 HTTP 請求。如果不調用 end 方法,瀏覽器地址欄左邊的叉號會一直存在,表示該請求尚未完成。
end 方法支持一個字符串或者 buffer 作為參數,可以指定在 HTTP 請求的最后返回的數據,該數據會在瀏覽器頁面上顯示出來;如果定義了回調方法,那么會在 end 返回后調用
```
res.end('Hello node', () => {
console.log('http cycle end')
})
```
### Stream
文件系統提供的 API 有一個重要的問題,就是如果文件過大,一次性地讀或寫顯然是不可行的,Stream 允許我們把較大的數據分批次地進行讀寫。
在 node.js 中,一共有四種基礎的 stream 類型:
- Readable:可讀流(for example fs.createReadStream())
- Writable:可寫流(for example fs.createWriteStream())
- Duplex:既可讀,又可寫(for example net.Socket)
- Transform:操作寫入的數據,然后讀取結果,通常用于輸入數據和輸出數據不要求匹配的場景,如 zlib.createDeflate()
```js
// 復制文件
const fs = require('fs')
const path = require('path')
const fileName1 = path.resolve(__dirname, 'data.txt')
const fileName2 = path.resolve(__dirname, 'data-bak.txt')
const readStream = fs.createReadStream(fileName1)
const writeStream = fs.createWriteStream(fileName2)
readStream.pipe(writeStream)
readStream.on('data', chunk => {
console.log(chunk)
console.log(chunk.toString())
})
readStream.on('end', () => {
console.log('copy done')
})
```
```
// 也可用于http
const http = require('http')
const fs = require('fs')
const path = require('path')
const fileName1 = path.resolve(__dirname, 'data.txt')
const server = http.createServer((req, res) => {
if (req.method === 'GET') {
const readStream = fs.createReadStream(fileName1)
readStream.pipe(res)
}
})
```
Writable Stream 主要使用 write 方法來寫入數據,該方法是異步的,假設我們創建一個可讀流讀取一個較大的文件,再調用 pipe 方法將數據通過一個可寫流寫入另一個位置。如果讀取的速度大于寫入的速度,那么 node 將會在內存中緩存這些數據。
當然緩沖區也是有大小限制的(state.highWatermark),當達到閾值后,write方法會返回 false,可讀流也進入暫停狀態,當 writable stream 將緩沖區清空之后,會觸發 drain 事件,上游的 readable 重新開始讀取數據
pipe 方法使得數據可以通過管道**由可讀流流入可寫流**。pipe 方法接收一個 writable 對象,當 readable 對象調用 pipe 方法時,會在內部調用 writable 對象的 write 方法進行寫入。
### ReadLine
ReadLine 是一個 node 原生模塊,提供了按行讀取 Stream 中數據的功能
```
const readline = require('readline')
const fs = require('fs')
const rl = readline.createInterface({
input: fs.createReadStream('data.txt')
})
// 每讀取一行數據出發
rl.on('line', data => {
console.log(data)
})
rl.on('close', () => {
console.log('closed')
})
```
### Events
node 程序中的對象會產生一系列的事件,例如一個 HTTP Server 會在每次有新連接時觸發一個事件,一個 Readable Stream 會在文件打開時觸發一個事件等。所有能觸發事件的對象都是 EventEmitter 類的實例。下面的代碼演示了如何注冊一個事件并觸發它:
```js
const eventEmitter = require('events')
const myEmitter = new eventEmitter()
myEmitter.on('begin', () => {
console.log('begin')
})
myEmitter.emit('begin')
```
在實際的開發中,通常不會直接使用 Event 模塊來進行事件處理,而是選擇將其作為基類進行繼承的方式來使用 Event,在 node 的內部實現中,凡是提供了事件機制的模塊,都會在內部繼承 Event 模塊,以 fs 模塊為例,下面是其源碼中的一部分
```
function FSWatcher() {
EventEmitter.call(this)
// ......
}
util.inherits(FSWatcher, EventEmitter) // util.inherits 是用來繼承的方法
```
假設我們要用 node 來開發一個網頁上的音樂播放器,關于播放和暫停的處理,就可以考慮通過繼承 Events 模塊來實現:
```
const util = require('util')
const event = require('events')
function Player() {
event.call(this)
}
util.inherits(Player, event)
const player = new Player()
player.on('pause', () => {
console.log('pause')
})
player.on('play', () => {
console.log('playing')
})
player.emit('play') // playing
```
### path 模塊
原文鏈接:[https://www.jianshu.com/p/78fadd20ee61](https://www.jianshu.com/p/78fadd20ee61)
1.連接路徑:path.join(\[path1\]\[, path2\]\[, ...\])
path.join() 方法可以連接任意多個路徑字符串。要連接的多個路徑可做為參數傳入。path.join() 方法在接邊路徑的同時也會對路徑進行規范化。例如:
```js
var path = require('path')
// 合法的字符串連接
path.join('/foo', 'bar', 'baz/asdf', 'quux', '..')
// 連接后
'/foo/bar/baz/asdf'
//不合法的字符串將拋出異常
path.join('foo', {}, 'bar')
// 拋出的異常
TypeError: Arguments to path.join must be strings'
```
2.路徑解析:path.resolve(\[from ...\], to)
path.resolve() 方法可以將多個路徑解析為一個規范化的絕對路徑。其處理方式類似于對這些路徑逐一進行 cd 操作,與 cd 操作不同的是,這些路徑可以是文件,并且可不必實際存在(resolve() 方法不會利用底層的文件系統判斷路徑是否存在,而只是進行路徑字符串操作)。例如:
```js
path.resolve('foo/bar', '/tmp/file/', '..', 'a/../subfile')
```
相當于
```shell
cd foo/bar
cd /tmp/file/
cd ..
cd a/../subfile
pwd
```
示例:
```js
path.resolve('/foo/bar', './baz')
// 輸出結果為
'/foo/bar/baz'
path.resolve('/foo/bar', '/tmp/file/')
// 輸出結果為
'/tmp/file'
path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif')
// 當前的工作路徑是 /home/itbilu/node,則輸出結果為
'/home/itbilu/node/wwwroot/static_files/gif/image.gif'
```
3.對比
```js
const path = require('path');
let myPath = path.join(__dirname,'/img/so');
let myPath2 = path.join(__dirname,'./img/so');
let myPath3 = path.resolve(__dirname,'/img/so');
let myPath4 = path.resolve(__dirname,'./img/so');
console.log(__dirname); // D:\myProgram\test
console.log(myPath); // D:\myProgram\test\img\so
console.log(myPath2); // D:\myProgram\test\img\so
console.log(myPath3); // D:\img\so
console.log(myPath4); // D:\myProgram\test\img\so
```
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直線、矩形、多邊形
- Part2-曲線圖形
- Part3-線條操作
- Part4-文本操作
- Part5-圖像操作
- Part6-變形操作
- Part7-像素操作
- Part8-漸變與陰影
- Part9-路徑與狀態
- Part10-物理動畫
- Part11-邊界檢測
- Part12-碰撞檢測
- Part13-用戶交互
- Part14-高級動畫
- CSS
- SCSS
- codePen
- 速查表
- 面試題
- 《CSS Secrets》
- SVG
- 移動端適配
- 濾鏡(filter)的使用
- JS
- 基礎概念
- 作用域、作用域鏈、閉包
- this
- 原型與繼承
- 數組、字符串、Map、Set方法整理
- 垃圾回收機制
- DOM
- BOM
- 事件循環
- 嚴格模式
- 正則表達式
- ES6部分
- 設計模式
- AJAX
- 模塊化
- 讀冴羽博客筆記
- 第一部分總結-深入JS系列
- 第二部分總結-專題系列
- 第三部分總結-ES6系列
- 網絡請求中的數據類型
- 事件
- 表單
- 函數式編程
- Tips
- JS-Coding
- Framework
- Vue
- 書寫規范
- 基礎
- vue-router & vuex
- 深入淺出 Vue
- 響應式原理及其他
- new Vue 發生了什么
- 組件化
- 編譯流程
- Vue Router
- Vuex
- 前端路由的簡單實現
- React
- 基礎
- 書寫規范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 與 Hook
- 《深入淺出React和Redux》筆記
- 前半部分
- 后半部分
- react-transition-group
- Vue 與 React 的對比
- 工程化與架構
- Hybird
- React Native
- 新手上路
- 內置組件
- 常用插件
- 問題記錄
- Echarts
- 基礎
- Electron
- 序言
- 配置 Electron 開發環境 & 基礎概念
- React + TypeScript 仿 Antd
- TypeScript 基礎
- React + ts
- 樣式設計
- 組件測試
- 圖標解決方案
- Storybook 的使用
- Input 組件
- 在線 mock server
- 打包與發布
- Algorithm
- 排序算法及常見問題
- 劍指 offer
- 動態規劃
- DataStruct
- 概述
- 樹
- 鏈表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 課程實戰記錄
- 服務器
- 操作系統基礎知識
- Linux
- Nginx
- redis
- node.js
- 基礎及原生模塊
- express框架
- node.js操作數據庫
- 《深入淺出 node.js》筆記
- 前半部分
- 后半部分
- 數據庫
- SQL
- 面試題收集
- 智力題
- 面試題精選1
- 面試題精選2
- 問答篇
- 2025面試題收集
- Other
- markdown 書寫
- Git
- LaTex 常用命令
- Bugs