帶你重新認識 Node
最初做 Node 的目的是什麼?
Node 作者 Ryan Dahl:
基於 V8 創建一個輕量級的高性能 Web 服務器並提供一套庫
爲什麼是 JavaScript?
Ryan Dahl 是一名資深的 C/C++ 程序員,創造出 Node 之前主要工作是圍繞 Web 高性能服務器進行的
他發現 Web 高性能服務器的兩個要點:
-
事件驅動
-
非阻塞 I / O
Ryan Dahl 也曾評估過使用 C、Lua、Haskell、Ruby 等語言作爲備選實現,得出以下結論:
-
C 的開發門檻高,可以預見不會有太多的開發者能將它用於業務開發
-
Ryan Dahl 覺得自己還不足夠玩轉 Haskell,所以捨棄它
-
Lua 自身已經含有很多阻塞 I / O 庫,爲其構建非阻塞 I / O 庫不能改變開發者使用習慣
-
Ruby 的虛擬機性能不佳
JavaScript 的優勢:
-
開發門檻低
-
在後端領域沒有歷史包袱
-
第二次瀏覽器大戰漸漸分出高下,Chrome 瀏覽器的 JavaScript 引擎 V8 摘得性能第一的桂冠
Node 給 JavaScript 帶來的意義
除了 HTML、Webkit 和顯卡這些 UI 相關技術沒有支持外,Node 的結構與 Chrome 十分相似。他們都是基於事件驅動的異步架構:
-
瀏覽器通過事件驅動來服務界面上的交互
-
Node 通過事件驅動來服務 I / O
在 Node 中,JavaScript 還被賦予了新的能力:
-
隨心所欲地訪問本地文件
-
搭建 WebSocket 服務端
-
連接數據庫,進行業務研發
-
像 Web Worker 一樣玩轉多進程
Node 使 JavaScript 可以運行在不同的地方,不再限制在瀏覽器中、DOM 樹打交道。如果 HTTP 協議是水平面,Node 就是瀏覽器在協議棧另一邊的倒影。
Node 不處理 UI,但用與瀏覽器相同的機制和原理運行,打破了 JavaScript 只能在瀏覽器中運行的局面。前後端編程環境統一,可以大大降低前後端轉換所需要的上下文代價。
Node 的特點
異步 I / O
- 以讀取文件爲例
var fs = require('fs');
fs.readFile('/path', function (err, file) {
console.log('讀取文件完成')
});
console.log('發起讀取文件');
熟悉的用戶必知道,“讀取文件完成”是在 “發起讀取文件” 之後輸出的
fs.readFile 後的代碼是被立即執行的,而 “讀取文件完成” 的執行時間是不被預期的
只知道它將在這個異步操作後執行,但並不知道具體的時間點
異步調用中對於結果值的捕獲是符合 “Don't call me, I will call you” 原則的
這也是注重結果,不關心過程的一種表現
Node 中,絕大多數操作都以異步的方式進行調用,Ryan Dahl 排除萬難,在底層構建了很多異步 I / O 的 API,從文件讀取到網絡請求等。使開發者很已從語言層面很自然地進行並行 I / O 操作,在每個調用之間無需等待之前的 I / O 調用結束,在編程模型上可以極大提升效率
「注:異步 I / O 機制將在下文中詳細闡述」
事件與回調函數
「事件」
隨着 Web2.0 的到來,JavaScript 在前端擔任了更多的職責,時間也得到了廣泛的應用。將前端瀏覽器中廣泛應用且成熟的事件與回到函數引入後端,配合異步 I / O ,可以很好地將事件發生的時間點暴露給業務邏輯。
- 服務端例子
對於服務器綁定了 request 事件
對於請求對象,綁定了 data 和 end 事件
var http = require('http');
var querystring = require('querystring');
// 偵聽服務器的request事件
http.createServer(function (req, res) {
var postData = '';
req.setEncoding('utf8');
// 偵聽請求的data事件
req.on('data', function (trunk) {
postData += trunk;
});
// 偵聽請求的end事件
req.on('end', function () {
res.end(postData);
});
}).listen(8080);
console.log('服務器啓動完成');
- 前端例子
發出請求後,只需關心請求成功時執行相應的業務邏輯即可
request({
url: '/url',
method: 'POST',
data: {},
success: function (data) {
// success事件
}
});
事件的編程方式具有輕量級、松耦合、只關注事務點等優勢,但是在多個異步任務的場景下,事件與事件之間各自獨立,如何協作是一個問題,後續也出現了一系列異步編程解決方案:
-
事件發佈 / 訂閱模式
-
Promise、async / await
-
流程控制庫
「回調函數」
-
Node 除了異步和事件外,回調函數也是一大特色
-
縱觀下來,回調函數也是最好的接收異步調用返回數據的方式
-
但是這種編程方式對於很多習慣同步思路編程的人來說,也許是十分不習慣的
-
代碼的編寫順序與執行順序並無關係,這對他們可能造成閱讀上的障礙
-
在流程控制方面,因爲穿插了異步方法和回調函數,與常規的同步方式相比變得不那麼一目瞭然了
-
轉變爲異步編程思維後,通過對業務的劃分和對事件的提煉,在流程控制方面處理業務的複雜度是與同步方式實際上是一致的
單線程
Node 保持了 JavaScript 在瀏覽器中單線程的特點
JavaScript 與其他線程是無法共享任何狀態的,最大的好處是不用像多線程編程那樣處處在意狀態的同步問題,這裏沒有死鎖的存在,也沒有線程上下文交換所帶來的性能上的開銷
-
單線程的缺點
-
無法利用多核 CPU
-
錯誤會引起整個應用退出,健壯性較差
-
大量計算佔用 CPU 導致無法繼續調用異步 I / O
-
後續也推出了 child_process 和 cluster 模塊較好地緩解了以上缺點
跨平臺
起初 Node 只能在 Linux 平臺上運行,如果想在 Windows 平臺上學習和使用 Node,則必須通過 Cygwin / MinGW,後微軟投入通過基於 libuv 實現跨平臺架構
- libuv
在操作系統與 Node 上層模塊系統之間構建了一層平臺架構
通過良好的架構,Node 的第三方 C++ 模塊也可以藉助 libuv 實現跨平臺
Node 模塊機制 - CommonJS
背景:
在其他高級語言中,Java 有類文件,Python 有 import 機制,Ruby 有 require,PHP 有 include 和 require。而 JavaScript 通過 script 標籤引入代碼的方式顯得雜亂無章,。人們不得不用命名空間等方式人爲地約束代碼,以達到安全和易用的目的。
直到後來出現了 CommonJS...
願景
希望 JavaScript 能在任何地方運行
出發點
對於 JavaScript 自身而言,它的規範依然是薄弱的,還有以下缺陷:
-
沒有模塊系統
-
標準庫較少
-
ECMAScript 僅定義了部分核心庫
-
對於文件系統 I / O 流等常見需求沒有標準 API
-
沒有標準接口
-
在 JavaScript 中,幾乎沒有定義過如 Web 服務器或者數據庫之類的標準統一接口
-
缺乏包管理系統
-
導致 JavaScript 應用中基本沒有自動加載和安裝以來的能力
CommonJS 的提出,主要是爲了彌補當前 JavaScript 沒有標準的缺陷,以達到像 Python、Ruby 和 Java 具備開發大型應用的基礎能力,而不是停留在小腳本程序的階段,希望可以利用 JavaScript 開發:
-
服務端 JavaScript 程序
-
命令行工具
-
桌面圖形界面應用程序
-
混合應用
CommonJS 規範涵蓋了:
-
模塊
-
二進制
-
Buffer
-
字符集編碼
-
I / O 流
-
進程環境
-
文件系統
-
套接字
-
單元測試
-
Web 服務器網關接口
-
包管理
Node 與瀏覽器以及 W3C 組織、CommonJS 組織、ECMAScript 之間的關係,共同構成了一個繁榮的生態系統
模塊規範
- 模塊定義
上下文提供了 exports 對象用於導出當前模塊的方法或者變量,並且它是導出的唯一出口
在模塊中,還存在一個 module 對象,它代表模塊自身,而 exports 是 module 的屬性
在 Node 中,一個文件就是一個模塊,將方法掛載在 exports 對象上作爲屬性即可定義導出的方式
// math.js
exports.add = function(a, b){
return a + b;
}
- 模塊引用
const math = require('./math');
const res = math.add(1, 1);
console.log(res);
// 2
在 CommonJS 規範中,存在 require 方法,這個方法接受模塊標識,以此引入一個模塊的 API 到當前上下文中
- 模塊標識
模塊標識就是傳遞給 require 方法的參數,可以是:
-
如何小駝峯命名的字符串
-
以./ 、../ 開頭的相對路徑 or 絕對路徑
-
可以沒有文件名後綴. js
模塊的定義十分簡單,接口也十分簡潔
每個模塊具有獨立的空間,它們互不干擾,在引用時也顯得乾淨利落
- 意義:
將類聚的方法和變量等限定在私有的作用域中,同時支持引入和導出功能以順暢地連接上下游依賴
模塊實現
在 Node 引入模塊,需要經歷以下三個步驟
-
路徑分析
-
文件定位
-
編譯執行
Node 中模塊分爲兩類:
- 核心模塊
編譯過程中,編譯進了二進制執行文件
在 Node 進程啓動時,部分核心模塊就直接被加載進內存中,所以這部分核心模塊引入時,文件定位和編譯執行這兩個步驟可以省略,並且在路徑分析中優先判斷,所以它的加載速度是最快的。
- 用戶編寫的文件模塊
運行時動態加載,需要完整的路徑分析、文件定位、編譯執行過程,速度比核心模塊慢
優先從緩存加載
與瀏覽器會緩存靜態腳本文件以提高性能一樣,Node 對引入過的模塊都會進行二次緩存,以減少二次引入時的開銷。不同點在於:
-
瀏覽器僅緩存文件
-
Node 緩存的是編譯和執行之後的對象
無論核心模塊還是文件模塊,require 方法對相同模塊的二次加載都一律採用緩存優先的方式
路徑分析和文件定位
「標識符分析(路徑)」
前面說到過,require 方法接受一個參數作爲標識符,分爲以下幾類:
- 核心模塊
優先級僅次於緩存加載,在 Node 的源代碼編譯過程中已編譯爲二進制代碼,加載過程最快
「注:加載一個與核心模塊標識符相同的自定義模塊是不會成功的,只能通過選擇不同的標識符 / 換用路徑的方式實現」
- 路徑形式的文件模塊
以 ./ 、../ 開頭的標識符都被當做文件模塊處理
require 方法會將路徑轉爲真實路徑,並以真實路徑爲索引,將編譯執行後的結果存放到緩存中,以使二次加載更快
文件模塊給 Node 指明瞭確切的文件位置,所以在查找過程中可以節約大量時間,其加載速度僅慢於核心模塊
- 自定義模塊
是一種特殊的文件模塊,是一個文件或者包的形式
這類模塊的查找是最費時的,也是最慢的一種
先介紹一下模塊路徑這個概念,也是定位文件模塊時制定的查找策略,具體表現爲一個路徑組成的數組
-
console.log(module.path)
-
你可以得到一個路徑數組
['/home/bytedance/reasearch/node_modules',
'/home/bytedance/node_modules',
'home/node_module', /node_modules']
可以看出規則如下:
-
當前文件目錄下的 node_modules 目錄
-
父目錄下的 node_modules 目錄
-
父目錄的父目錄下的 node_modules 目錄
-
沿路徑向上逐級遞歸,直到根目錄下的 node_modules 目錄
它的生成方式與 JavaScript 原型鏈 / 作用域鏈的查找方式十分類似
在加載過程中,Node 會逐個嘗試模塊路徑中的路徑,直到找到目標文件
文件路徑越深,模塊查找耗時會越多,這是自定義模塊的加載速度最慢的原因
「文件定位」
- 文件擴展名分析
require 分析標識符會出現不包含文件擴展名的情況
會按. js、.json、.node 的次序補足擴展名,一次嘗試
過程中,需調用 fs 模塊同步阻塞地判斷文件是否存在,Node 單線程因此會引起性能問題
如果是. node / .json 文件帶上擴展名能加快點速度,配合緩存機制,可大幅緩解 Node 單線程阻塞調用的缺陷
- 目錄分析和包
分析標識符的過程中可能沒有找到文件,卻得到一個目錄,則會將目錄當做一個包來處理
通過解析 package.json 文件對應該包的 main 屬性指定的文件名
如果 main 相應文件解析錯誤 / 沒有 package.json 文件,node 會將 index 作爲文件名
一次查找 index.js index.json index.node
該目錄沒有定位成功則進行下一個模塊路徑進行查找
直到模塊路徑數組都被遍歷完依然沒有查找到目標文件則拋出異常
模塊編譯
在 Node 中,每個文件模塊都是一個對象
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
if (parent && parent.children) {
parent.children.push(this);
}
this.filename = null;
this.loaded = false;
this.children = [];
}
-
js 文件
-
通過 fs 模塊同步讀取文件後編譯執行
-
node 文件
-
這是用 C/C++ 編寫的擴展文件,通過 dlopen 方法加在最後編譯生成的文件
-
json 文件
-
通過 fs 模塊同步讀取文件後,JSON.parse 解析返回的結果
-
其他
-
都被當作 js 文件載入
每一個編譯成功的模塊都會將其文件路徑作爲索引存在 Module.cache 對象上,以提高二次引入的性能
包與 NPM
Node 組織了自身核心模塊,也使得第三方文件模塊可以有序地編寫和使用
但是在第三方模塊中,模塊與模塊之間仍然是散列在各地的,相互之間不能直接引用
而在模塊之外,包和 NPM 則是將模塊聯繫起來的一種機制
一定程度上解決了變量依賴、依賴關係等代碼組織性問題
包結構
包實際上是一個存檔文件,即一個目錄直接打包爲一個. zip/tar.gz 格式的文件,安裝後解壓還原爲目錄
-
符合 CommonJS 規範的包目錄應該包含如下文件
-
package.json 包描述文件
-
bin 用於存放可執行二進制文件
-
lib 用於存放 JavaScript 代碼的目錄
-
doc 用於存放文檔的目錄
-
test 用於存放單元測試用例的代碼
包描述文件
package.json
CommonJS 爲 package.json 定義瞭如下一些必須的字段
-
name 包名
-
description 包簡介
-
version 版本號
-
keywords 關鍵詞數組,用於做 npm 搜索
-
maintainers 包維護者列表
-
contributors 貢獻者列表
-
bugs 一個可以反饋 bug 的網頁地址 / 郵件地址
-
licenses 許可證列表
-
repositories 託管源代碼的位置列表
-
dependencies 使用當前包所需要依賴的包
-
homepage 當前包的網站地址
-
os 操作系統支持列表
-
aix、freebsd、linux、macos、solaris、vxworks、windows
-
cpu CPU 架構的支持列表
-
arm、mips、ppc、sparc、x86、x86_64
-
builtin 標誌當前包是否是內建在底層系統的標準組件
-
implements 實現規範的列表
-
scripts 腳本說明對象
包規範的定義可以幫助 Node 解決依賴包安裝的問題,而 NPM 正是基於該規範進行了實現
NPM 常用功能
CommonJS 包規範是理論,NPM 是其中一種實踐
NPM 於 Node,相當於 gem 於 Ruby,pear 於 PHP
幫助完成了第三方模塊的發佈、安裝和依賴等
- 查看幫助
-
查看版本
npm -v
-
查看命令
npm
- 安裝依賴包
npm install {packageName}
執行該命令後,NPM 會在當前目錄下創建 node_modules 目錄下創建包目錄,接着將相應的包解壓到這個目錄下
- 全局安裝模式
npm install {packageName} -g
全局模式並不是將一個模塊包安裝爲一個全局包的意思,它並不意味着可以從任何地方 reuqire 它
全局模式這個稱謂並不精確,-g 實際上是將一個包安裝爲全局可用的執行命令
它根據包描述文件中的 bin 字段配置,將實際腳本鏈接到與 Node 可執行文件相同的路徑下
- 從本地安裝
對於一些沒有發佈到 NPM 上的包,或者因爲網絡原因無法直接安裝的包
可以通過將包下載到本地,然後本地安裝
npm install <tarball file>
npm install <tarball url>
npm install folder>
- 從非官方源安裝
如果不能通過官方源安裝,可以通過鏡像源安裝
npm install --registry={urlResource}
如果使用過程中幾乎全使用鏡像源,可以指定默認源
npm config set registry {urlResource}
- NPM 鉤子命令
package.json 中 scripts 字段的提出就是讓包在安裝或者卸載等過程中提供鉤子機制
"scripts":{
"preinstall": "preinstall.js",
"install": "install.js",
"uninstall": "uninstall.js",
"test": "test.js",
}
-
Install
-
在以上字段執行
npm install <package>
時,preinstall 指向的腳本會被加載執行,然後 install 指向的腳本會被執行 -
Uninstall
-
執行
npm uninstall <package>
時,uninstall 指向的腳本也許會做一些清理工作 -
Test
-
執行
npm test
將會運行 test 指向的腳本,一個優秀的包應當包含測試用例,並在 package.json 文件正配置好運行測試的命令,方便用戶運行測試用例,以便檢驗包是否穩定可靠
局域 NPM
- 背景
企業的限制在於,一方面需要享受到模塊開發帶來的低耦合和項目組織上的好處,另一方面卻要考慮模塊保密性的問題。所以,通過 NPM 共享和發佈存在潛在的風險。
- 解決方案
爲了同時能夠享受到 NPM 上衆多的包,同時對自己的包進行保密和限制,現有的解決方案就是企業搭建自己的 NPM 倉庫,NPM 無論是它的服務端和客戶端都是開源的。
局域 NPM 倉庫的搭建方法與搭建鏡像站的方式幾乎一樣,與鏡像倉庫不同的地方在於可以選擇不同步官方源倉庫中的包
-
作用
-
私有的可重用模塊可以打包到局域 NPM 倉庫中,這樣可以保持更新的中心化,不至於讓各個小項目維護相同功能的模塊
-
杜絕通過複製粘貼實現代碼共享的行爲
異步 I / O
爲什麼需要異步 I / O ?
- 用戶體驗
瀏覽器中 JavaScript 在單線程上執行,還和 UI 渲染共用一個線程
《高性能 JavaScript》曾總結過,如果腳本執行的時間超過 100ms 用戶就會感到頁面卡頓
如果網頁臨時需要獲取一個網絡資源,通過同步的方式獲取,JS 需要等資源完全從服務器獲取後才能繼續執行,這期間 UI 將停頓,不響應用戶的交互行爲。可以想象,這樣的用戶體驗將會多差。
而採用異步請求,JavaScript 和 UI 的執行都不會處於等待狀態,給用戶一個鮮活的頁面
I / O 是昂貴的,分佈式 I / O 是更昂貴的
只有後端能夠快速響應資源,才能讓前端體驗變好
- 資源分配
計算機在發展過程中將組件進行了抽象,分爲了 I / O 設備和計算設備
假設業務場景有一組互不相關的任務需要完成,主流方法有兩種:
- 多線程並行完成
多線程的代價在於創建線程和執行線程上下文切換的開銷較大。
在複雜的業務中經常面臨鎖、狀態同步等問題。但是多線程在多核 CPU 上能夠有效提升 CPU 利用率
- 單線程串行依次執行
單線程順序執行任務比較符合編程人員按順序思考的思維方式,依然是主流的編程方式
串行執行的缺點在於性能,任意一個略慢的任務都會導致後續執行代碼被阻塞
在計算機資源中,通常 I / O 與 CPU 計算是可以並行的,同步編程模型導致的問題是,I / O 的進行會讓後續任務等待,這造成資源不能更好地被利用
- Node 在兩者之間給出了它的答案
利用單線程,遠離多線程死鎖、狀態同步等問題;
利用異步 I / O,讓單線程可以遠離阻塞,更好地使用 CPU
爲了彌補單線程無法利用多核 CPU 的缺點,Node 提供了類似前端瀏覽器中 Web Workers 的子進程,該子進程可以通過工作進程高效地利用 CPU 和 I / O
異步 I / O 的提出是期望 I / O 的調用不再阻塞後續運算,將原有等待 I / O 完成的這段時間分配給其餘需要的業務去執行
異步 I / O 現狀
異步 I / O 與非阻塞 I / O
操作系統內核對於 I / O 方式只有兩種:阻塞與非阻塞
在調用阻塞 I / O 時,應用程序需要等待 I / O 完成才返回結果
特點:調用之後一定要等到系統內核層面完成所有操作後調用才結束
例子:系統內核在完成磁盤尋道、讀取數據、複製數據到內幕才能中之後,這個調用才結束》
非阻塞 I / O 與阻塞 I / O 的差別爲調用之後會立即返回
非阻塞 I / O 返回之後,CPU 的時間片可以用來處理其他事務,此時的性能提升是明顯的
存在的問題:
-
由於完整的 I / O 沒有完成,立即返回的並不是業務層期望的數據而僅僅是當前調用的狀態
-
爲了獲取完整的數據,應用程序需要重複調用 I / O 操作來確認是否完成,稱之爲 “輪詢”。
主要的輪詢技術
- read
它是最原始、性能最低的一種,通過重複調用檢查 I / O 的狀態來完成數據的完整讀取
在得到最終數據前,CPU 一直耗用在等待上
- select
它是在 read 的基礎上改進的一種方案,通過對文件描述符上的事件狀態來進行判斷
限制:它採用一個 1024 長度的數組來存儲狀態,最多可以同時檢查 1024 個文件描述符
- poll
較 select 有所改進,採用鏈表的方式避免數組長度的限制,其次它能避免不需要的檢查
文件描述符較多時,它的性能還是十分低下的
- epoll
該方案是 Linux 下效率最高的 I / O 事件通知機制,在進入輪詢的時候如果沒有檢查到 I / O 事件,將會進行休眠,直到事件將它喚醒。它是真實利用了事件通知、執行回調的方式,而不是遍歷查詢,所以不會浪費 CPU,執行效率較高
理想的非阻塞異步 I / O
儘管 epoll 已經利用了時間來降低 CPU 的耗用,但是休眠期間 CPU 幾乎是限制的,對於當前線程而言利用率不夠
完美的異步 I / O 應該是應用程序發起非阻塞調用,無需通過遍歷或者時間喚醒等方式輪詢
可以直接處理下一個任務,只需在 I / O 完成後通過信號或回調將數據傳遞給應用程序即可
Linux 下存在原生提供的一種異步 I / O 方式(AIO)就是通過信號或者回調來傳遞數據的
缺點:
-
僅 Linux 下有
-
僅支持 I / O 中的 O_DIRECT 方式讀取,導致無法利用系統緩存
注:關於 O_DIRECT
現實的異步 I / O
通過讓部分線程進行阻塞 I / O 或者非阻塞 I / O 加輪詢技術來完成數據獲取,讓一個線程進行計算處理,通過線程之間的通信將 I / O 得到的數據進行傳遞,這就輕鬆實現了異步 I / O(儘管它是模擬的
-
libeio 實質上是採用線程池與阻塞 I / O 模擬異步 I / O
-
Node 最初在 * nix 平臺下采用 libeio 配合 libev 實現異步 I / O,後通過自行實現線程池完成
-
Windows 下的 IOCP
-
調用異步方法,等待 I / O 完成之後的通知,執行回調,用戶無需考慮輪詢
-
內部其實仍是線程池的原理,不同之處在於這些線程池由系統內核接手管理
-
與 Node 異步調用模型十分近似
-
由於 Windows 平臺和 * nix 平臺的差異,Node 提供了 libuv 作爲抽象封裝層,做兼容性判斷
-
保證上層 Node 與下層的自定義線程池和 IOCP 各自獨立
-
我們時常提到 Node 是單線程的
-
這裏的單線程僅僅只是 JavaScript 執行在單線程中罷了
-
無論是 * nix 還是 Windows 平臺,內部完成 I / O 任務的另有線程池
Node 的異步 I / O
Node 完成整個異步 I / O 環節的有事件循環、觀察者和請求對象等
事件循環
着重強調一下 Node 自身的執行模型——事件循環
Node 進程啓動時,會創建一個類似 while(true) 的循環
每次循環體的過程稱之爲 Tick,每個 Tick 的過程就是查看是否有事件待處理
如果有就取出事件及其相關的回調函數,並執行它們
觀察者
每個事件循環中有一個或多個觀察者,而判斷是否有事件要處理的過程就是向這些觀察者詢問是否有要處理的事件
-
瀏覽器採用了類似的機制
-
事件可能來自用戶的點擊或者加載某些文件時產生,而這些產生的事件都有對應的觀察者
-
Node 中事件主要來源於網絡請求、文件 I / O 等
-
這些時間對應的觀察者有文件 I / O 觀察者、網絡 I / O 觀察者等,將事件進行了分類
-
事件循環是一個典型的生產者 / 消費者模型
-
異步 I / O、網絡請求等則是事件的生產者
-
這些事件被傳遞到對應的觀察者,事件循環則從觀察者那取出事件並處理
小結
事件循環、觀察者、請求對象、I / O 線程池這四者共同構成了 NOde 異步 I / O 模型的基本要素
由於我們知道 JavaScipt 是單線程的,所以按嘗試很容易理解它不能充分利用多核 CPU
事實上在 Node 中,除了 JavaScript 是單線程外,Node 自身其實是多喜愛暱稱的,只是 I / O 線程使用的 CPU 較少
另一個需要注意的點是,除了用戶代碼無法並行執行以外,所有的 I / O 是可以並行執行的
注:圖爲 Node 整個異步 I / O 過程
事件驅動與高性能服務器
前面對異步的講解,也基本勾勒出了事件驅動的實質,即通過主循環加事件觸發的方式來運行程序
下面爲幾種經典的服務器模型:
-
同步式
-
一次只能處理一個請求,並且其餘請求都處於等待狀態
-
進程 / 請求
-
這樣可以處理多個請求,但是它不具備擴展性,因爲系統資源只有那麼多
-
線程 / 請求
-
儘管線程比進程要清涼,但是由於每個線程都佔用一定內存,當大併發請求到來時,內存將會很快用光,導致服務器緩慢
-
比進程 / 請求要好,但對於大型站點而言依然不夠
-
總結
-
這使得服務器能夠有條不紊地處理請求,即使在大量連接的情況下,也不受上下文切換開銷的影響,這也是 Node 高性能的一個原因
-
線程 / 請求的方式目前還被 Apache 所採用
-
Node 通過事件驅動的方式處理請求,無需爲每一個請求創建額外的線程,可以省掉創建線程和銷燬線程的開銷
-
同時操作系統在調度任務時因爲線程較少,上下文的代價很低
事件驅動帶來的高效已經漸漸開始爲業界所重視
知名服務器 Nginx 也摒棄了多線程的方式,採用和 Node 相同的事件驅動
不同之處在於 Nginx 採用純 C 寫成,性能較高,但是它僅適合於做 Web 服務器,用於反向代理或者負載均衡服務,在業務處理方面較爲欠缺
Node 則是一套高性能平臺,可以利用它構建與 Nginx 相同的功能,也可以處理各種具體業務
Node 沒有 Nginx 在 Web 服務器方面那麼專業,但場景更大,自身性能也不錯
在實際項目中可以結合它們各自的優點以達到應用的最優性能
JavaScript 在服務器端近乎空白,使得 Node 沒有任何歷史包袱,而 Node 在性能優化上的表現使得它一下子就在社區中流行了起來~
公衆號:前端食堂
知乎:童歐巴
掘金:童歐巴
這是一個終身學習的男人,他在堅持自己熱愛的事情,歡迎你加入前端食堂,和這個男人一起開心的變胖~
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Veq5ZTylp9cbGmD8JUzapw