Node.js是什么?
Node.js最初開始于2009年,讓JavaScript代碼離開瀏覽器的執(zhí)行環(huán)境也可以執(zhí)行
可以將Node.js理解為一個(gè)將多種技術(shù)組合起來的平臺(tái),可以使用JavaScript調(diào)用系統(tǒng)接口,開發(fā)后端應(yīng)用
既然說到將多種技術(shù)組合起來,那么可以先看看Node.js用到了哪些技術(shù)

圖片是nodejs v1.0也就是最早發(fā)布的node版本下的deps文件,也就是nodejs所用到的依賴
-
cares:用C-ares做域名解析 -
gtest:是C/C++的單元測(cè)試框架 -
http-parser:用來解析http -
npm:包管理工具 -
openssl:用來解析https -
uv:一個(gè)跨平臺(tái)的異步I/O庫(kù) -
v8:google開發(fā)的js引擎,為js提供運(yùn)行環(huán)境 -
zlib:用來做加密
那么這些技術(shù)又是怎么進(jìn)行組合的呢,再看看Node.js的技術(shù)架構(gòu)

將Node.js分成三層
- 首先最上層是node api,提供http模塊、流模塊、文件模塊等等,可以使用js直接調(diào)用
- 中間層
node bindings主要是使js和C/C++進(jìn)行通信 - 最下面這一層是支撐nodejs運(yùn)行的關(guān)鍵,主要由
v8、libuv、c-ares等模塊組成,向上一層提供api服務(wù)
相信我們或多或少都接觸過第一層Node api,剛剛也通過Node安裝的依賴初步了解了最下層的模塊具有什么功能,那么中間的這個(gè)Node bindings又是什么呢?
什么是Node bindings?
背景:C/C++實(shí)現(xiàn)了一個(gè)用來解析http的庫(kù)
http-parser,非常高效,可是對(duì)于只會(huì)寫js的程序員非常的不友好,因?yàn)闆]有辦法直接去調(diào)用這個(gè)C/C++的庫(kù),這兩個(gè)語(yǔ)言連最基本的數(shù)據(jù)類型都不一樣,還怎么做朋友
結(jié)論:js無(wú)法直接調(diào)用C++的庫(kù),需要一個(gè)中間的橋梁(調(diào)用途徑)
那么bindings需要怎么實(shí)現(xiàn)呢?
Node.js的作者Ryan做了一個(gè)中間層處理
- Node.js用C++對(duì)http-parse進(jìn)行封裝,使它符合某些要求(比如統(tǒng)一數(shù)據(jù)類型),封裝好的文件叫做
http_parse_binding.cpp - 用Node.js提供的編譯工具將其編譯為
.node文件 - js代碼可以直接通過
require關(guān)鍵字引入這個(gè).node文件
這樣js就能夠調(diào)用C++庫(kù),這個(gè)中間的橋梁就是bindings,由于node提供了很多binding,所以就叫做node bindings
JS如何與C++通信?
// test.js
const addon = require('./build/Release/addon');
console.log('This should be eight:', addon.add(3, 5));
上面是js調(diào)用,再來看看C++代碼(已被編譯)
// addon.cc
#include <node.h>
namespace demo {
using v8::Exception;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::NewStringType;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;
// 這是 "add" 方法的實(shí)現(xiàn)。
// 輸入?yún)?shù)使用 const FunctionCallbackInfo<Value>& args 結(jié)構(gòu)傳入。
void Add(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
// 檢查傳入的參數(shù)的個(gè)數(shù)。
if (args.Length() < 2) {
// 拋出一個(gè)錯(cuò)誤并傳回到 JavaScript。
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate,
"參數(shù)的數(shù)量錯(cuò)誤",
NewStringType::kNormal).ToLocalChecked()));
return;
}
// 檢查參數(shù)的類型。
if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate,
"參數(shù)錯(cuò)誤",
NewStringType::kNormal).ToLocalChecked()));
return;
}
// 執(zhí)行操作
double value =
args[0].As<Number>()->Value() + args[1].As<Number>()->Value();
Local<Number> num = Number::New(isolate, value);
// 設(shè)置返回值 (使用傳入的 FunctionCallbackInfo<Value>&)。
args.GetReturnValue().Set(num);
}
void Init(Local<Object> exports) {
NODE_SET_METHOD(exports, "add", Add);
}
NODE_MODULE(NODE
_GYP_MODULE_NAME, Init)
} // 命名空間示例
Nodejs封裝的插件開放一些對(duì)象和函數(shù),供運(yùn)行在Node.js中的JS訪問,當(dāng)JS調(diào)用函數(shù)addon時(shí),輸入?yún)?shù)和返回值與C/C++代碼相互映射,統(tǒng)一封裝處理。這樣就可以直接在Node.js中引入并使用
// test.js
const addon = require('./build/Release/addon');
// 傳入一個(gè)函數(shù)
addon((msg) => {
console.log(msg);
// 打印: 'hello world'
});
傳入C++并執(zhí)行
// addon.cc
#include <node.h>
namespace demo {
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::NewStringType;
using v8::Null;
using v8::Object;
using v8::String;
using v8::Value;
void RunCallback(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Local<Function> cb = Local<Function>::Cast(args[0]);
const unsigned argc = 1;
// 這里有一個(gè)c++方法,將args[0]也就是我們傳入的函數(shù),轉(zhuǎn)化成c++看得懂的,用cb接收
Local<Value> argv[argc] = {
String::NewFromUtf8(isolate,
"hello world",
NewStringType::kNormal).ToLocalChecked() };
// 調(diào)用一下,傳入的函數(shù)就被調(diào)用了,打印出hello world
cb->Call(context, Null(isolate), argc, argv).ToLocalChecked();
}
void Init(Local<Object> exports, Local<Object> module) {
NODE_SET_METHOD(module, "exports", RunCallback);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Init)
}
在這個(gè)例子中,回調(diào)函數(shù)被同步地調(diào)用,要知道C++是看不懂JS的,所以如何做中間層的封裝就交給這些node插件去做
有了這些Node.js提供的插件(node binding),JS和C++就可以進(jìn)行交互了,也使JS的能力被大大的擴(kuò)展了
再回顧一下Node.js的技術(shù)架構(gòu)

剛剛詳細(xì)介紹了什么是Node bindings,它是如何工作的,接著再來看最下面一層功能模塊

什么是V8
它是Google開發(fā)的js引擎,為js提供運(yùn)行環(huán)境
為啥是v8?它是現(xiàn)階段執(zhí)行js最快的一個(gè)引擎
那么v8的功能有哪些呢
- 將JS源代碼變成本地代碼并執(zhí)行
- 維護(hù)調(diào)用棧,確保JS函數(shù)的執(zhí)行順序
- 內(nèi)存管理,為所有對(duì)象分配內(nèi)存
- 垃圾回收,重復(fù)利用無(wú)用的內(nèi)存
- 實(shí)現(xiàn)JS的標(biāo)準(zhǔn)庫(kù)
逐個(gè)分析一下:
- 啥是本地代碼?其實(shí)本地代碼就是機(jī)器代碼,就比如說0和1,計(jì)算機(jī)看到這些代碼直接就可以執(zhí)行,不再需要借助其他的任何工具,非常的高效。V8在運(yùn)行之前將js編譯成了機(jī)器代碼
- JS函數(shù)的執(zhí)行順序是由v8引擎決定的
- 那么v8如何做內(nèi)存管理呢?比如說
new一個(gè)對(duì)象,它的內(nèi)存在哪里,也是引擎來決定的 - 而垃圾回收,是因?yàn)閮?nèi)存是有限的,比如用了2k的內(nèi)存,用完了還得還回來給下一個(gè)程序用,所以目的就是為了重復(fù)利用
- 標(biāo)準(zhǔn)庫(kù)這個(gè)怎么理解?其實(shí)就是實(shí)現(xiàn)數(shù)組的
sort,splice等等api,v8來實(shí)現(xiàn),js來調(diào)用
需要注意的是:js是單線程的,而V8本身是多線程的,開一個(gè)線程執(zhí)行js,開一個(gè)線程清理內(nèi)存,然后再處理一些其他別的活兒,線程和線程之間毫無(wú)瓜葛
什么是libuv
背景:因?yàn)楦鱾€(gè)系統(tǒng)的I/O庫(kù)都不一樣,windows系統(tǒng)有IOCP,Linux系統(tǒng)有epoll。Node.js的作者Ryan為了將其整合在一起實(shí)現(xiàn)一個(gè)跨平臺(tái)的異步I/O庫(kù),開始寫libuv
好了,背景說完了,啥是I/O?
例如:
- 從操作系統(tǒng)寫文件到硬盤
- 訪問網(wǎng)絡(luò),從操作系統(tǒng)發(fā)出數(shù)據(jù)到別的服務(wù)器
- 打印連接打印機(jī),從操作系統(tǒng)發(fā)指令給打印機(jī)
以上這些行為都是I/O,可以理解為系統(tǒng)和外界進(jìn)行交互的過程都叫I/O
而
libuv會(huì)根據(jù)你是什么系統(tǒng),自動(dòng)的選擇當(dāng)前系統(tǒng)已經(jīng)實(shí)現(xiàn)好了的異步操作(I/O)庫(kù),用于TCP/UDP/DNS文件等的異步操作
- 比如操作TCP,我們都知道http是基于TCP/IP的,如果可以操作TCP那么,就可以做http的服務(wù)
- UDP,用于實(shí)時(shí)通信,常見的QQ聊天
- 解析DNS
包括讀文件、寫文件什么的,libuv都可以幫你管理。這樣I/O的部分就全部交給c語(yǔ)言去做,js完全不用管,甩手掌柜,負(fù)責(zé)調(diào)用就行了
v8和libuv在整個(gè)Node.js架構(gòu)的底層是最為重要的,其他功能就不做詳細(xì)介紹了
Node.js工作流程

了解了Node Bindings、v8、還有libuv貌似可以把工作流程串一串了
Application就是咱們寫的代碼,把它放在
v8上面去運(yùn)行。發(fā)現(xiàn)需要去讀一個(gè)文件,這時(shí)候libuv開一個(gè)線程去讀文件。讀完文件,操作系統(tǒng)會(huì)返回一個(gè)事件給event loop,event loop就把文件傳回給v8,再給到代碼
Emmm...
還是先了解一下Event Loop吧

什么是Event Loop
Event Loop,是Event和Loop
- Event
計(jì)時(shí)器到期了、文件可以讀取了、讀取出錯(cuò)了
比如說在js里面寫一個(gè)setTimeOut,10秒之后打印一行字,所以當(dāng)10秒鐘到了,就會(huì)產(chǎn)生一個(gè)事件,執(zhí)行回調(diào)
什么時(shí)候文件可以讀,什么時(shí)候文件可以寫,或者說讀取出錯(cuò)的時(shí)候,就需要操作系統(tǒng)生成一個(gè)事件(Event)告訴js,因?yàn)閖s啥也不知道
一般來說事件分兩種,內(nèi)部的和外部事件,比如計(jì)時(shí)器就是內(nèi)部事件,文件讀取就是外部的,因?yàn)槲募谟脖P上面,硬盤和操作系統(tǒng)又是分開的
- Loop
Loop就是循環(huán),由于事件分優(yōu)先級(jí),所以處理起來也是分先后順序,所以Node.js需要按順序輪詢每種事件,輪詢是循環(huán)的
既然說到事件優(yōu)先級(jí),舉個(gè)例子,有三種不同的事件
setTimeout(fn1, 100) // 計(jì)時(shí)器到期了
fs.readFile(‘/1.txt’, fn2) // 文件可以讀了
server.on(‘close’, fn3) // 服務(wù)器關(guān)閉了
以上三種事件如果同時(shí)發(fā)生,執(zhí)行順序是怎么樣的?
- 執(zhí)行讀文件,文件來了立馬去讀
因?yàn)槿绻募梢宰x了現(xiàn)在不讀,沒準(zhǔn)兒過會(huì)兒就不能讀了 - 執(zhí)行服務(wù)器事件
用戶請(qǐng)求進(jìn)來,可以稍微等一會(huì)兒,但是如果太久了也可能就不請(qǐng)求了 - 執(zhí)行定時(shí)器的事件
定時(shí)器可以拖一下
這個(gè)順序是人為規(guī)定的,接著循環(huán)
人為規(guī)定了一個(gè)優(yōu)先級(jí)也就是一個(gè)執(zhí)行順序,這個(gè)人為規(guī)定就是event loop
總結(jié)下來就是三句話
- 對(duì)不同的事件分優(yōu)先級(jí)
- node.js順序的去輪詢每一種事件
- 把這個(gè)過程看成循環(huán)圈
示意如圖:

timer:先看看有沒有計(jì)時(shí)器,有了執(zhí)行
I/O:有咩有其他沒有歸類的回調(diào)
Idle:空閑一會(huì)兒,清理戰(zhàn)場(chǎng)
Poll:輪詢階段,處理大部分的事件(文件可讀了?讀!http請(qǐng)求來了,處理?。?br>
Check:處理setImmediate回調(diào)
Close callback:看看有沒有socket關(guān)閉的回調(diào)
------------循環(huán)-----------------
但是node.js不傻,不會(huì)一直循環(huán)循環(huán),如果發(fā)現(xiàn)沒什么事兒做,就會(huì)停留在poll(輪詢)階段
輪詢的階段呢,會(huì)看看有沒有文件可以讀,有沒有請(qǐng)求可以處理,就等著,時(shí)不時(shí)的看看有沒有新的代碼,或者檢查一下最近的計(jì)時(shí)器,看看有沒有需要過會(huì)兒去執(zhí)行的callback
如果計(jì)時(shí)器事件要處理了,我再?gòu)南鲁霭l(fā),繞回timers
Node.js大部分時(shí)間都會(huì)停留在poll階段,大部分事件都在poll階段被處理,如文件、網(wǎng)絡(luò)請(qǐng)求
相信大家對(duì)Event Loop有了一個(gè)初步的了解和認(rèn)識(shí),那么看回Node.js工作流程

- Application就是咱們寫的代碼,把它放在v8上面去運(yùn)行
- 運(yùn)行的過程中,發(fā)現(xiàn)我們寫了個(gè)
setTimeout,v8就會(huì)調(diào)用Node.js的bindings,把這個(gè)settimeout放進(jìn)Even loop里面 - Event loop就會(huì)等待適合的時(shí)機(jī)去發(fā)送一個(gè)事件去執(zhí)行這些js代碼,接著循環(huán)等待,一般停留在poll階段久一些
- 發(fā)現(xiàn)需要去讀一個(gè)文件,這時(shí)候Event loop就會(huì)通過
libuv開一個(gè)線程去專門做讀文件這事兒 - 讀完文件,操作系統(tǒng)會(huì)返回一個(gè)事件給Event loop,Event loop就把文件傳回給
v8,最后給到代碼
需要注意js從頭至尾都不參與讀文件這個(gè)事情,libuv去讀
(可以看到最最重要的部分是libuv和v8,而我們寫的代碼只占小小的一部分)
一句話就是,代碼到
v8,通過Node api 使用libuv和其他一些C/C++提供的功能去完成用戶所需要的功能
Nodejs將這些模塊進(jìn)行整合,所以說Node.js不是一門語(yǔ)言,就是一個(gè)平臺(tái)

最后的最后,回顧總結(jié)一下
- 用
libuv進(jìn)行異步I/O操作
Node.js是使用libuv進(jìn)行異步I/O操作,一般來說讀文件是一個(gè)同步的動(dòng)作,這時(shí)候有了libuv,Nodejs就把這活兒交給了libuv,讓libuv去讀這個(gè)文件,這時(shí)候Node.js就沒有什么事兒可以做了,等libuv讀完了發(fā)過來一個(gè)事件,Node.js再接手處理,這就是個(gè)很重要的異步過程 - 用Event loop管理事件處理順序
基于libuv,Nodejs又實(shí)現(xiàn)了一個(gè)Even loop用來管理不同事件的處理順序 - 用
C/C++庫(kù)高效處理DNS/HTTP…
Nodejs還使用一些C++的庫(kù),高效的處理了dns/http等常用功能,有了這些功能,基本上就可以處理文件,處理網(wǎng)絡(luò)等一些雜七雜八的事情 - 用bindings讓JS能和
C/C++溝通
咱們?cè)偃绾问褂胘s也使用這些功能呢,這時(shí)候bindings的價(jià)值就體現(xiàn)出來了,讓js能夠直接和c++溝通,直接require一下.node文件 - 用V8運(yùn)行JS
接著Nodejs又引入了v8,讓js代碼離開瀏覽器的執(zhí)行環(huán)境也能夠運(yùn)行 - 用Node.js標(biāo)準(zhǔn)庫(kù)簡(jiǎn)化JS代碼
Node很貼心的給用戶準(zhǔn)備了很高效的庫(kù),比如http,fs之類的,大大簡(jiǎn)化了你的js代碼
那么為啥nodejs可以高效的處理這些請(qǐng)求呢?因?yàn)橹苯邮褂胏語(yǔ)言的代碼,要比js快