雖然 Node.js 是單線程的,但是在融合了[libuv](https://github.com/libuv/libuv)后,使其有能力非常簡單地就構建出高性能和可擴展的網絡應用程序。
  下圖是 Node.js 的簡單架構圖,基于 V8 和 libuv,其中 Node Bindings 為 JavaScript 和 C++ 搭建了一座溝通的橋梁,使得 JavaScript 可以訪問 V8 和 libuv 向上層提供的 API。
:-: 
  本系列所有的示例源碼都已上傳至Github,[點擊此處](https://github.com/pwstrick/node)獲取。
## 一、術語解析
  接下來會對幾個與 Node.js 相關的術語做單獨的解析,其中事件循環會單獨細講。
**1)libuv**
  libuv 是一個事件驅動、非阻塞異步的 I/O 庫,并且具備跨平臺的能力,提供了一套事件循環(Event Loop)機制和一些核心工具,例如定時器、文件訪問、線程池等。
**2)非阻塞異步的I/O**
  非阻塞是指線程不會被操作系統掛起,可以處理其他事情。
  異步是指調用者發起一個調用后,可以立即返回去做別的事。
  I/O(Input/Output)即輸入/輸出,通常指數據在存儲器或其他周邊設備之間的輸入和輸出。
  它是信息處理系統(例如計算機)與外部世界(可能是人類或另一信息處理系統)之間的通信。
  將這些關鍵字組合在一起就能理解 Node.js 的高性能有一部分是通過避免等待 I/O(讀寫數據庫、文件訪問、網絡調用等)響應來實現的。
**3)事件驅動**
  事件驅動是一種異步化的程序設計模型,通過用戶動作、操作系統或應用程序產生的事件,來驅動程序完成某個操作。
  在 Node.js 中,事件主要來源于網絡請求、文件讀寫等,它們會被事件循環所處理。
  在瀏覽器的 DOM 系統中使用的也非常廣泛,例如為按鈕綁定 click 事件,在用點擊按鈕時,彈出提示或提交表單等。
**4)單線程**
  Node.js 的單線程是指運行 JavaScript 代碼的主線程,網絡請求或異步任務等都交給了底層的線程池中的線程來處理,其處理結果再通過事件循環向主線程告知。
  單線程意味著所有任務需要排隊有序執行,如果出現一個計算時間很長的任務,那么就會占據主線程,其他任務只能等待,所以說 Node.js 不適合 CPU 密集型的場景。
   經過以上術語的分析可知,Node.js 的高性能和高并發離不開異步,所以有必要深入了解一下 Node.js 的異步原理。
# 二、事件循環
  當 Node.js 啟動時會初始化事件循環,這是一個無限循環。
  下圖是事件循環的一張運行機制圖,新任務或完成 I/O 任務的回調,都會添加到事件循環中。
:-: 
  下面是按照運行優先級簡化后的[六個循環階段](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/)。
~~~
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
~~~
  每個階段都有一個 FIFO 回調隊列,當隊列耗盡或達到回調上限時,事件循環將進入下一階段,如此往復。
1. timers:執行由 setTimeout() 和 setInterval() 安排的回調。在此階段內部,會維護一個定時器的小頂堆,按到期時間排序,先到期的先運行。
2. pending callbacks:處理上一輪循環未執行的 I/O 回調,例如網絡、I/O 等異常時的回調。
3. idle,prepare:僅 Node 內部使用。
4. poll:執行與 I/O 相關的回調,除了關閉回調、定時器調度的回調和 setImmediate() , 適當的條件下 Node 將阻塞在這里。
5. check:調用 setImmediate() 回調。
6. close callbacks:關閉回調,例如 socket.on("close", callback)。
  在[deps/uv/src/unix/core.c](https://github.com/nodejs/node/blob/master/deps/uv/src/unix/core.c)文件中聲明了事件循環的核心代碼,旁邊還有個 win 目錄,應該就是指 Windows 系統中 libuv 相關的處理。
  其實事件循環就是一個大的 while 循環?,具體如下所示。
  代碼中的 UV\_RUN\_ONCE 就是上文 poll 階段中的適當的條件,在每次循環結束前,執行完 close callbacks 階段后,會再執行一次已到期的定時器。
~~~
static int uv__loop_alive(const uv_loop_t* loop) {
return uv__has_active_handles(loop) ||
uv__has_active_reqs(loop) ||
loop->closing_handles != NULL;
}
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
// 檢查事件循環中是否還有待處理的handle、request、closing_handles是否為NULL
r = uv__loop_alive(loop);
// 更新事件循環時間戳
if (!r)
uv__update_time(loop);
// 啟動事件循環
while (r != 0 && loop->stop_flag == 0) {
uv__update_time(loop);
uv__run_timers(loop); // timers階段,執行已到期的定時器
ran_pending = uv__run_pending(loop); // pending階段
uv__run_idle(loop); // idle階段
uv__run_prepare(loop);// prepare階段
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
uv__io_poll(loop, timeout); // poll階段
/* Run one final update on the provider_idle_time in case uv__io_poll
* returned because the timeout expired, but no events were received. This
* call will be ignored if the provider_entry_time was either never set (if
* the timeout == 0) or was already updated b/c an event was received.
*/
uv__metrics_update_idle_time(loop);
uv__run_check(loop); // check階段
uv__run_closing_handles(loop); // close階段
if (mode == UV_RUN_ONCE) {
/* UV_RUN_ONCE implies forward progress: at least one callback must have
* been invoked when it returns. uv__io_poll() can return without doing
* I/O (meaning: no callbacks) when its timeout expires - which means we
* have pending timers that satisfy the forward progress constraint.
*
* UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from
* the check.
*/
uv__update_time(loop);
uv__run_timers(loop); // 執行已到期的定時器
}
r = uv__loop_alive(loop);
// 在 UV_RUN_ONCE 和 UV_RUN_NOWAIT 模式中,跳出當前循環
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
/* The if statement lets gcc compile it to a conditional store. Avoids
* dirtying a cache line.
*/
if (loop->stop_flag != 0)
loop->stop_flag = 0; // 標記當前的 stop_flag 為 0,表示跑完這輪,事件循環就結束了
return r;
}
~~~
**1)setTimeout 和 setImmediate**
  setTimeout 會在最前面的 timers 階段被執行,而 setImmediate 會在 check 階段被執行。
  但在下面的示例中,timeout 和 immediate 的打印順序是不確定的。
  在 setTimeout()[官方文檔](https://nodejs.org/dist/latest-v18.x/docs/api/timers.html#settimeoutcallback-delay-args)中曾提到,當延遲時間大于 2147483647(24.8天) 或小于 1 時,將默認被設為 1。
  所以下面的 setTimeout(callback, 0) 相當于 setTimeout(callback, 1)。
  雖然在源碼中會先運行 uv\_\_run\_timers(),但是由于上一次的循環耗時可能超過 1ms,也可能小于 1ms,所以定時器有可能還未到期。
  如此的話,就會造成打印順序的不確定性,上述分析過程[參考了此處](https://cnodejs.org/topic/57d68794cb6f605d360105bf#57d7b1f53f3cb94e6b326746)。
~~~
setTimeout(() => {
console.log('timeout')
}, 0);
setImmediate(() => {
console.log('immediate')
});
~~~
  如果將 setTimeout() 和 setImmediate() 注冊到 I/O 回調中運行,那么順序就是確定的,先 immediate 再 timeout。
~~~
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate')
})
});
~~~
  這是因為 readFile() 的回調會在 poll 階段運行,而在 uv\_\_io\_poll() 之后,就會立即執行 uv\_\_run\_check(),從而就能保證先打印 immediate 。
  在自己的日常工作中,曾使用過一個基于 setTimeout() 的定時任務庫:[node-schedule](https://github.com/node-schedule/node-schedule)。
  由于延遲時間最長為 24.8 天,所以該庫巧妙的運用了一個遞歸來彌補時間的上限。
~~~
Timeout.prototype.start = function() {
if (this.after <= TIMEOUT_MAX) {
this.timeout = setTimeout(this.listener, this.after)
} else {
var self = this
this.timeout = setTimeout(function() {
self.after -= TIMEOUT_MAX
self.start()
}, TIMEOUT_MAX)
}
if (this.unreffed) {
this.timeout.unref()
}
}
~~~
**2)與瀏覽器中的事件循環的差異**
  在瀏覽器的事件循環中,沒有那么細的循環階段,不過有兩個非常重要的概念,那就是宏任務和微任務。
  宏任務包括 setTimeout()、setInterval()、requestAnimationFrame、Ajax、fetch()、腳本標簽代碼等。
  微任務包括 Promise.then()、MutationObserver。
  在 Node.js 中,[process.nextTick()](https://nodejs.org/dist/latest-v18.x/docs/api/process.html#processnexttickcallback-args)是微任務的一種,setTimeout()、setInterval()、setImmediate() 等都屬于宏任務。
  在 Node版本 < 11 時,執行完一個階段的所有任務后,再執行process.nextTick(),最后是其他微任務。
  可以這樣理解,process.nextTick() 維護了一個獨立的隊列,不存在于事件循環的任何階段,而是在各個階段切換的間隙執行。
  即從一個階段切換到下個階段前執行,執行時機如下所示。
~~~
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ nextTickQueue
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ nextTickQueue
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘
nextTickQueue nextTickQueue
│ ┌─────────────┴─────────────┐
│ │ poll │
│ └─────────────┬─────────────┘
│ nextTickQueue
│ ┌─────────────┴─────────────┐
│ │ check │
│ └─────────────┬─────────────┘
│ nextTickQueue
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
~~~
  但是在 Node 版本 >= 11 之后,會處理的和瀏覽器一樣,也是每執行完一個宏任務,就將其微任務也一并完成。
  在下面這個示例中, setTimeout() 內先聲明 then(),再聲明 process.nextTick(),最后執行一條打印語句。
  接著在 setTimeout() 之后再次聲明了 process.nextTick()。?
~~~
// setTimeout
setTimeout(() => {
Promise.resolve().then(function() {
console.log('promise');
});
process.nextTick(() => {
console.log('setTimeout nextTick');
});
console.log('setTimeout');
}, 0);
// nextTick
process.nextTick(() => {
console.log('nextTick');
});
~~~
  我本地運行的 Node 版本是 16,所以最終的打印順序如下所示。
~~~
nextTick
setTimeout
setTimeout nextTick
promise
~~~
  外面的 process.nextTick() 要比 setTimeout() 先運行,里面的打印語句最先執行,然后是 process.nextTick(),最后是 then()。
**3)sleep()**
  有一道比較經典的題目是編寫一個 sleep() 函數,實現線程睡眠,在日常開發中很容易就會遇到。
  搜集了多種實現函數,有些是同步,有些是異步。
  第一種是同步函數,創建一個循環,占用主線程,直至循環完畢,這種方式也叫循環空轉,比較浪費CPU性能,不推薦。
~~~
function sleep(ms) {
var start = Date.now(), expire = start + ms;
while (Date.now() < expire);
}
~~~
  第二至第四種都是異步函數,本質上線程并沒有睡眠,事件循環仍在運行,下面是 Promise + setTimeout() 組合實現的 sleep() 函數。
~~~
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
~~~
  第三種是利用 util 庫的[promisify()](https://nodejs.org/dist/latest-v18.x/docs/api/util.html#utilpromisifyoriginal)函數,返回一個 Promise 版本的定時器。
~~~
function sleep(ms) {
const { promisify } = require('util');
return promisify(setTimeout)(ms);
}
~~~
  第四種是當 Node 版本 >= 15 時可以使用,在[timers庫](https://nodejs.org/dist/latest-v18.x/docs/api/timers.html#timerspromisessettimeoutdelay-value-options)中直接得到一個 Promise 版本的定時器。
~~~
function sleep(ms) {
const { setTimeout } = require('timers/promises');
return setTimeout(ms);
}
~~~
  第五種是同步函數,可利用[Atomics.wait](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait)阻塞事件循環,直至線程超時,實現細節在此不做說明了。
~~~
function sleep(ms) {
const sharedBuf = new SharedArrayBuffer(4);
const sharedArr = new Int32Array(sharedBuf);
return Atomics.wait(sharedArr, 0, 0, ms);
}
~~~
  還可以編寫 C/C++ 插件,直接調用操作系統的 sleep() 函數,此處不做展開。
參考資料:
[Event Loop](https://mp.weixin.qq.com/s/RNYYNR7A01V-Y2aC1wNsGw)?[事件循環源碼](https://yjhjstz.gitbooks.io/deep-into-node/content/chapter5/chapter5-1.html)?[Node.js技術棧](https://www.nodejs.red/#/nodejs/translate/everything-you-need-to-know-about-node-js-lnc?id=the-event-loop%ef%bc%88%e4%ba%8b%e4%bb%b6%e5%be%aa%e7%8e%af%ef%bc%89)
[nodejs真的是單線程嗎?](https://segmentfault.com/a/1190000014926921)
[Nodejs探秘:深入理解單線程實現高并發原理](https://imweb.io/topic/5b6cf97093759a0e51c917c8)
[什么是CPU密集型、IO密集型?](https://zhuanlan.zhihu.com/p/62766037)?[libuv](https://luohaha.github.io/Chinese-uvbook/source/introduction.html)?[I/O](https://zh.m.wikipedia.org/zh-cn/I/O)
[JavaScript 運行機制詳解:再談Event Loop](https://www.ruanyifeng.com/blog/2014/10/event-loop.html)
[Node.js Event Loop 的理解 Timers,process.nextTick()](https://cnodejs.org/topic/57d68794cb6f605d360105bf)
[瀏覽器與Node的事件循環(Event Loop)有何區別?](https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/26)
[Why is the EventLoop for Browsers and Node.js Designed This Way?](https://blog.bitsrc.io/why-is-the-eventloop-for-browsers-and-node-js-designed-this-way-f7f794696c?gi=29723793aa09)
[Node.js 事件循環](https://learnku.com/articles/38802)?[Phases of the Node JS Event Loop](https://medium.com/@kunaltandon.kt/process-nexttick-vs-setimmediate-vs-settimeout-explained-wrt-different-event-loop-phases-c0506b12921d)
[如何實現線程睡眠?](https://www.nodejs.red/#/nodejs/tips/sleep?id=%e4%ba%8c%ef%bc%9a%e5%ae%9a%e6%97%b6%e5%99%a8-promise-%e5%ae%9e%e7%8e%b0-sleep)
[nodejs中的并發編程](https://segmentfault.com/a/1190000022113106)
*****
> 原文出處:
[博客園-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