在 JavaScript 中,一般只處理字符串層面的數據,但是在 Node.js 中,需要處理網絡、文件等二進制數據。
  由此,引入了[Buffer](https://nodejs.org/dist/latest-v18.x/docs/api/buffer.html)和[Stream](https://nodejs.org/dist/latest-v18.x/docs/api/stream.html)的概念,兩者都是字節層面的操作。
  Buffer 表示一塊專門存放二進制數據的緩沖區。Stream 表示流,一種有序、有起點和終點的二進制傳輸手段。
  Stream 會從 Buffer 中讀取數據,像水在管道中流動那樣轉移數據。
  本系列所有的示例源碼都已上傳至Github,[點擊此處](https://github.com/pwstrick/node)獲取。
## 一、Buffer
  Buffer 是 JavaScript 中的 Uint8Array 的子類,Uint8Array 是一種類型化數組,處理 8 位無符號整數。
  其行為類似于數組(有 length 屬性,可迭代等),但并不是真正的數組,其元素是 16 進制的兩位數。
  Buffer 在創建時就會確定占用內存的大小,之后就無法再調整,并且它會被分配一塊 V8 堆棧外的原始內存。
  Buffer 的應用場景比較多,例如在[zlib](https://nodejs.org/dist/latest-v18.x/docs/api/zlib.html)模塊中,利用 Buffer 來操作二進制數據實現資源壓縮的功能;在[crypto](https://nodejs.org/dist/latest-v18.x/docs/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options)模塊的一些加密算法,也會使用 Buffer。
**1)創建**
  在 Node 版本 <= 6 時,創建 Buffer 實例是 通過構造函數創建的:new Buffer(),但后面的版本就廢棄了。
  現在常用的創建方法有:
* Buffer.from() :傳入已有數據,轉換成一個 Buffer 實例,數據可以是字符串、對象、數組等。
* Buffer.alloc():分配指定字節數量的 Buffer 實例。
* Buffer.allocUnsafe() :功能與 Buffer.alloc() 相同,但其所占內存中的舊數據不會被清除,可能會泄漏敏感數據。
**2)編碼**
  在創建一個 Buffer 實例后,就可以像數組那樣訪問某個字符,而打印出的值是數字,如下所示,這些數字是 Unicode 碼。
~~~
let buf = Buffer.from('strick')
console.log(buf[0]); // 115
console.log(buf[1]); // 116
~~~
  若在創建時包含中文字符,那么就會多 3 個 16 進制的兩位數,如下所示。
~~~
let buf = Buffer.from('strick')
console.log(buf); // <Buffer 73 74 72 69 63 6b>
buf = Buffer.from('strick平')
console.log(buf); // <Buffer 73 74 72 69 63 6b e5 b9 b3>
~~~
  Buffer.from() 的第二個參數是編碼,默認值是 utf8,而 1 個中文字符經過 UTF-8 編碼后通常會占用 3 個字節,1 個英文字符只占用 1 個字節。
  在調用 toString() 方法后就能根據指定編碼(不傳默認是 UTF-8)將 Buffer 解碼為字符串。
~~~
console.log(buf.toString()); // strick平
~~~
  Node.js 支持的其他編碼包括 latin1、base64、ascii 等,具體可參考[官方文檔](https://nodejs.org/dist/latest-v18.x/docs/api/buffer.html#buffers-and-character-encodings)。
**3)內存分配原理**
  Node.js 內存分配都是在 C++ 層面完成的,采用 Slab 分配器(Linux 中有廣泛應用)動態分配內存,并且以 8KB 為界限來區分是小對象還是大對象(參考自[深入淺出Node.js](https://book.douban.com/subject/25768396/))。
  可以簡單看下[Buffer.from()](https://nodejs.org/dist/latest-v18.x/docs/api/buffer.html#static-method-bufferfromstring-encoding)的源碼,當它的參數是字符串時,其內部會調用 fromStringFast() 函數(在[src/lib/buffer.js](https://github.com/nodejs/node/blob/master/lib/buffer.js)中),然后根據字節長度分別處理。
  如果當前所占內存不夠,那么就會調用 createPool() 擴容,通過調用 createUnsafeBuffer() 創建 Buffer,其中 FastBuffer 繼承自 Uint8Array。
~~~
// 以 8KB 為界限
Buffer.poolSize = 8 * 1024;
// Buffer.from() 內會調用此函數
function fromStringFast(string, ops) {
const length = ops.byteLength(string);
// 長度大于 4KB(>>> 表示無符號右移 1 位)
if (length >= (Buffer.poolSize >>> 1))
return createFromString(string, ops.encodingVal);
// 當前所占內存不夠(poolOffset 記錄已經使用的字節數)
if (length > (poolSize - poolOffset))
createPool();
let b = new FastBuffer(allocPool, poolOffset, length);
const actual = ops.write(b, string, 0, length);
if (actual !== length) {
// byteLength() may overestimate. That's a rare case, though.
b = new FastBuffer(allocPool, poolOffset, actual);
}
poolOffset += actual;
alignPool();
return b;
}
// 初始化一個 8 KB 的內存空間
function createPool() {
poolSize = Buffer.poolSize;
allocPool = createUnsafeBuffer(poolSize).buffer;
markAsUntransferable(allocPool);
poolOffset = 0;
}
// 創建 Buffer
function createUnsafeBuffer(size) {
zeroFill[0] = 0;
try {
return new FastBuffer(size);
} finally {
zeroFill[0] = 1;
}
}
// FastBuffer 繼承自 Uint8Array
class FastBuffer extends Uint8Array {}
~~~
## 二、流
  流(Stream)的概念最早見于 Unix 系統,是一種已被證實有效的編程方式。
  Node.js 內置的流模塊會被其他多個核心模塊所依賴,它具有可讀、可寫或可讀寫的特點,并且所有的流都是 EventEmitter 的實例,也就是說被賦予了異步的能力。
  官方總結了流的兩個優點,分別是:
* 內存效率: 無需加載大量的數據到內存中即可進行處理。
* 時間效率: 當獲得數據之后就能立即開始處理數據,而不必等到整個數據加載完,這樣消耗的時間就變少了。
**1)流類型**
  流的基本類型有4種:
* Readable:只能讀取數據的流,例如 fs.createReadStream(),可注冊的事件包括 data、end、error、close等。
* Writable:只能寫入數據的流,例如 fs.createWriteStream(),HTTP 的請求和響應,可注冊的事件包括 drain、error、finish、pipe 等。
* Duplex:Readable 和 Writable 都支持的全雙工流,例如 net.Socket,這種流會維持兩個緩沖區,分別對應讀取和寫入,允許兩邊同時獨立操作。
* Transform:在寫入和讀取數據時修改或轉換數據的 Duplex 流,例如 zlib.createDeflate()。
  來看一個官方的 Readable 流示例,先是用 fs.readFile() 直接將整個文件讀到內存中。當文件很大或并發量很高時,將消耗大量的內存。
~~~
const http = require('http')
const fs = require('fs')
http.createServer(function(req, res) {
fs.readFile(__dirname + '/data.txt', (err, data) => {
res.end(data)
})
}).listen(1234)
~~~
  再用 fs.createReadStream() 方法通過流的方式來讀取文件,其中 req 和 res 兩個參數也是流對象。
  data.txt 文件中的內容將會一段段的傳輸給 HTTP 客戶端,而不是等到讀取完了再一次性響應,兩者對比,高下立判。
~~~
http.createServer((req, res) => {
const readable = fs.createReadStream(__dirname + '/data.txt')
readable.pipe(res);
}).listen(1234)
~~~
**2)pipe()**
  在上面的示例中,pipe() 方法的作用是將一個可讀流 readable 變量中的數據傳輸到一個可寫流 res 變量(也叫目標流)中。
  pipe() 方法地主要目的是平衡讀取和寫入的速度,讓數據的流動達到一個可接受的水平,防止因為讀寫速度的差異,而導致內存被占滿。
  在 pipe() 函數內部會監聽可讀流的 data 事件,并且會自動調用可寫流的 end() 方法。
  當內部緩沖大于配置的最高水位線(highWaterMark)時,也就是讀取速度大于寫入速度時,為了避免產生背壓問題,Node.js 就會停止數據流動。
  當再次重啟流動時,會觸發 drain 事件,其具體實現可[參考此文](https://cnodejs.org/topic/56ba030271204e03637a3870)。
  pipe() 方法會返回目標流,雖然支持鏈式調用,但必須是 Duplex 或 Transform 流,否則會報錯,如下所示。
~~~
http.createServer((req, res) => {
const readable = fs.createReadStream(__dirname + '/data.txt')
const writable = fs.createWriteStream(__dirname + '/tmp.txt')
// Error [ERR_STREAM_CANNOT_PIPE]: Cannot pipe, not readable
readable.pipe(writable).pipe(res);
}).listen(1234)
~~~
**3)end()**
  很多時候寫入流是不需要手動調用 end() 方法來關閉的。但如果在讀取期間發生錯誤,那就不能關閉寫入流,發生內存泄漏。
  為了防止這種情況發生,可監聽可讀流的錯誤事件,手動關閉,如下所示。
~~~
readable.on('error', function(err) {
writeable.close();
});
~~~
  接下來看一種網絡場景,改造一下之前的示例,讓可讀流監聽 data、end 和 error 事件,當讀取完畢或出現錯誤時關閉可寫流。
~~~
http.createServer((req, res) => {
const readable = fs.createReadStream(__dirname + '/data.txt')
readable.on('data', chunk => {
res.write(chunk);
});
readable.on('end',() => {
res.end();
})
readable.on('error', err => {
res.end('File not found');
});
}).listen(1234)
~~~
  若不手動關閉,那么頁面將一直處于加載中,在[KOA源碼](https://www.cnblogs.com/strick/p/16178207.html)中,多處調用了此方法。
  注意,若取消對 data 事件的監聽,那么頁面也會一直處于加載中,因為流一開始是靜止的,只有在注冊 data 事件后才會開始活動。
**4)大JSON文件**
  網上看到的一道題,用 Node.js 處理一個很大的 JSON 文件,并且要讀取到 JSON 文件的某個字段。
  直接用 fs.readFile() 或 require() 讀取都會占用很大的內存,甚至超出電腦內存。
  直接用 fs.createReadStream() 也不行,讀到的數據不能格式化成 JSON 對象,難以讀取字段。
  CNode論壇上對此問題也做過專門的[討論](https://cnodejs.org/topic/55a4b5213ecc81b621bba8d0)。
  借助開源庫[JSONStream](https://github.com/dominictarr/JSONStream)可以實現要求,它基于[jsonparse](https://github.com/creationix/jsonparse),這是一個流式 JSON 解析器。
  JSONStream 的源碼去掉注釋和空行差不多 200 行左右,在此就不展開分析了。
參考資料:
[緩沖區](https://www.nodejs.red/#/nodejs/buffer)?[Stream多文件合并](https://www.nodejs.red/#/nodejs/modules/stream-mutil-file-merge)?[pipe](https://www.nodejs.red/#/nodejs/modules/stream-pipe)?
[legacy.js模塊實現分析](https://www.nodejs.red/#/nodejs/modules/stream-lib-internal-stremas-legacy)?[Stream兩種模式](https://www.nodejs.red/#/nodejs/advanced/stream-object-mode-and-flow-mode)?
[Stream背壓](https://www.nodejs.red/#/nodejs/advanced/stream-back-pressure)
[深入理解Node.js之Buffer](https://yjhjstz.gitbooks.io/deep-into-node/content/chapter6/chapter6-1.html)?[流](https://yjhjstz.gitbooks.io/deep-into-node/content/chapter8/chapter8-1.html)
[Node.js Buffer](http://nodejs.cn/learn/nodejs-buffers)?[Node.js 流](http://nodejs.cn/learn/nodejs-streams)
[Node.js 語法基礎 —— Buffter & Stream](https://zhaomenghuan.js.org/note/nodejs/nodejs-buffer-stream.html)
[node源碼分析](https://zhuanlan.zhihu.com/p/422029211)
[通過源碼解析 Node.js 中導流(pipe)的實現](https://cnodejs.org/topic/56ba030271204e03637a3870)
*****
> 原文出處:
[博客園-Node.js精進](https://www.cnblogs.com/strick/category/2154090.html)
[知乎專欄-前端性能精進](https://www.zhihu.com/column/c_1611672656142725120)
已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎瀏覽。

推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、擴展運算符和剩余參數
- 3、解構
- 4、模板字面量
- 5、對象字面量的擴展
- 6、Symbol
- 7、代碼模塊化
- 8、數字
- 9、字符串
- 10、正則表達式
- 11、對象
- 12、數組
- 13、類型化數組
- 14、函數
- 15、箭頭函數和尾調用優化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、類
- 21、類的繼承
- 22、Promise
- 23、Promise的靜態方法和應用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基礎實踐
- 3、WebRTC視頻通話
- 4、Web音視頻基礎
- CSS進階
- 1、CSS基礎拾遺
- 2、偽類和偽元素
- 3、CSS屬性拾遺
- 4、浮動形狀
- 5、漸變
- 6、濾鏡
- 7、合成
- 8、裁剪和遮罩
- 9、網格布局
- 10、CSS方法論
- 11、管理后臺響應式改造
- React
- 1、函數式編程
- 2、JSX
- 3、組件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表單
- 8、樣式
- 9、組件通信
- 10、高階組件
- 11、Redux基礎
- 12、Redux中間件
- 13、React Router
- 14、測試框架
- 15、React Hooks
- 16、React源碼分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基礎
- 4、webpack進階
- 5、Git
- 6、Fiddler
- 7、自制腳手架
- 8、VSCode插件研發
- 9、WebView中的頁面調試方法
- Vue.js
- 1、數據綁定
- 2、指令
- 3、樣式和表單
- 4、組件
- 5、組件通信
- 6、內容分發
- 7、渲染函數和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、數據類型
- 2、接口
- 3、類
- 4、泛型
- 5、類型兼容性
- 6、高級類型
- 7、命名空間
- 8、裝飾器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系統和網絡
- 3、命令行工具
- 4、自建前端監控系統
- 5、定時任務的調試
- 6、自制短鏈系統
- 7、定時任務的進化史
- 8、通用接口
- 9、微前端實踐
- 10、接口日志查詢
- 11、E2E測試
- 12、BFF
- 13、MySQL歸檔
- 14、壓力測試
- 15、活動規則引擎
- 16、活動配置化
- 17、UmiJS版本升級
- 18、半吊子的可視化搭建系統
- 19、KOA源碼分析(上)
- 20、KOA源碼分析(下)
- 21、花10分鐘入門Node.js
- 22、Node環境升級日志
- 23、Worker threads
- 24、低代碼
- 25、Web自動化測試
- 26、接口攔截和頁面回放實驗
- 27、接口管理
- 28、Cypress自動化測試實踐
- 29、基于Electron的開播助手
- Node.js精進
- 1、模塊化
- 2、異步編程
- 3、流
- 4、事件觸發器
- 5、HTTP
- 6、文件
- 7、日志
- 8、錯誤處理
- 9、性能監控(上)
- 10、性能監控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 監控系統
- 1、SDK
- 2、存儲和分析
- 3、性能監控
- 4、內存泄漏
- 5、小程序
- 6、較長的白屏時間
- 7、頁面奔潰
- 8、shin-monitor源碼分析
- 前端性能精進
- 1、優化方法論之測量
- 2、優化方法論之分析
- 3、瀏覽器之圖像
- 4、瀏覽器之呈現
- 5、瀏覽器之JavaScript
- 6、網絡
- 7、構建
- 前端體驗優化
- 1、概述
- 2、基建
- 3、后端
- 4、數據
- 5、后臺
- Web優化
- 1、CSS優化
- 2、JavaScript優化
- 3、圖像和網絡
- 4、用戶體驗和工具
- 5、網站優化
- 6、優化閉環實踐
- 數據結構與算法
- 1、鏈表
- 2、棧、隊列、散列表和位運算
- 3、二叉樹
- 4、二分查找
- 5、回溯算法
- 6、貪心算法
- 7、分治算法
- 8、動態規劃
- 程序員之路
- 大學
- 2011年
- 2012年
- 2013年
- 2014年
- 項目反思
- 前端基礎學習分享
- 2015年
- 再一次項目反思
- 然并卵
- PC網站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端學習之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 2024年
- 日志
- 2020