做者:正龍 (滬江Web前端開發工程師) 本文原創,轉載請註明做者及出處。javascript
隨着Node.js的普及,愈來愈多的開發者使用Node.js來搭建環境,也有不少公司開始把Web站點遷移到Node.js服務器。Node.js的優點顯而易見,本文再也不贅述,那麼它是如何作到的呢?內部的邏輯又是什麼?帶着這些問題,筆者開始了研究Node.js的漫漫長征路。今天,筆者將跟你們探討一下Node.js的啓動原理。html
Node.js內部主要依賴Google的V8引擎和libuv實現。V8,想必你們會比較熟悉,它獨創把JavaScript直接翻譯成彙編代碼的方式執行,讓不少不可能變成了可能,例如Node.js。libuv,是一個跨平臺的異步IO庫,它所說的IO除了包含本地文件操做,還包含TCP、UDP等網絡套接字操做,範圍甚至能夠擴展到全部流操做(Stream)。因此,咱們能夠把Node.js理解爲添加了網絡功能的V8。前端
爲了描述方便,下面提到的環境是基於Windows 7專業版。用MAC的夥伴們也不用慌,內容實質仍然適用,可能具體名詞有些區別。另外,夥伴們能夠下載一份Node.js的源代碼(點此下載),本文用的是6.10.0 LTS。java
咱們打開Node.js的二進制發佈包,裏面內容很簡單:node.exe、npm和node.h頭文件。node.h頭文件只有開發Node.js插件時纔會用到。當咱們啓動node.exe時,它到底作了哪些事情?node
首先,它是一個EXE可執行文件,那確定會有一個main函數。Node.js的main函數定義在node_main.cc中,它主要是初始化V8 Platform和v8引擎;而後會啓動一個Node.js實例。具體調用鏈路如圖:c++
Init函數主要是解析Node.js啓動參數,並過濾V8選項傳給JavaScript引擎。npm
Node.js的main函數原來這麼短,那它應該很快運行完並返回。實際上,命令行窗口會一直等待着,並無立刻退出,這又是怎麼回事呢?答案就在StartInstance裏。首先它會建立V8執行沙盒,生成並初始化Node.js運行環境對象,而後啓動Node.js的循環等待。具體如圖:bootstrap
也就是說Node.js的主線程主要消費來自UV默認事件循環(uv_default_loop)和V8的MainThreadQueue和MainThreadDelayedQueue的任務。uv_run是一個阻塞調用。若是隊列中有任務,則執行並返回true,若是沒有的話,會阻塞住當前線程;若是返回false,則整個Node.js進程會釋放資源並退出。注意參數UV_RUN_ONCE,意思是從隊列中只取一個任務執行,無論隊列中當前是否有多個任務。api
到這兒,大概能夠理解到Node.js的「單線程」是怎麼回事。那運行的Node.js進程確實只開啓了一個線程嗎?咱們打開任務管理器看看:bash
實際上,Node.js進程當前有7個線程。查閱文檔以後發現,Node.js經過指定參數--v8-pool-size能夠設置V8線程池大小。原來V8的字節碼編譯、優化還有GC都是經過多線程完成;又繼續深刻調查,發現環境變量UV_THREADPOOL_SIZE會影響libuv的線程池大小。
Node.js目前爲止作的事情能夠概括爲,初始化V8和libuv。接下來,咱們看看Node.js自身運行環境是怎樣構建起來的。Node.js自身的運行環境由Environment類表示,咱們須要把process對象構建起來。process對象在JavaScript應用代碼中是能夠訪問到,它的文檔能夠狠戳這兒。注意,process如今尚未賦值給Global對象。CreateEnvironment執行流程如圖:
調用setAutorunMicrotask禁止V8本身消費隊列中的任務。SetupProcessObject主要設置process的屬性,例如比較重要的binding,還有其它提供給開發者的字段,好比cpuUsage、hrtime、uptime等。binding用於獲取C/C++構建的模塊,Node.js中的net庫就是經過這種方式最終調用到libuv。
binding就是作模塊查找,其執行過程以下:
環境對象準備好以後,就開始真正加載Node.js自身提供的JavaScript類庫代碼。LoadEnvironment執行過程以下:
到這兒,咱們能夠總結2個問題:
Node.js裏面本身提供的JavaScript庫是怎麼實現的?
經過C/C++代碼封裝成Node.js內置模塊,而後再經過process.binding暴露給JavaScript。
JavaScript庫文件是怎麼打包在node.exe中?
Node.js內置的JavaScript文件,經過js2c.py編譯生成臨時文件node_natives.h。
原理思路基本搞明白以後,下面咱們來作個小實例:如何把C++對象暴露給JavaScript。 程序主要是C++和JavaScript的交互,經過Node.js插件的方式運行。因此你們須要先了解下如何編譯Node.js插件,官方文檔猛戳這兒。
首先定義要導出的C++類,構造器能夠傳入一個數值;調用成員方法PlusOne,數值自增1並返回當前值。
namespace demo {
class MyObject : public node::ObjectWrap {
public:
static void Init(v8::Local<v8::Object> exports);
static void NewInstance(const v8::FunctionCallbackInfo<v8::Value>& args);
inline double value() const { return _value; }
private:
explicit MyObject(double value = 0);
~MyObject();
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void PlusOne(const v8::FunctionCallbackInfo<v8::Value>& args);
static v8::Persistent<v8::Function> constructor;
double _value;
};
}
複製代碼
實現文件
void MyObject::NewInstance(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
const unsigned argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Context> context = isolate->GetCurrentContext();
Local<Object> instance = cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(instance);
}
void MyObject::Init(Local<Object> exports) {
Isolate* isolate = exports->GetIsolate();
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject"));
tpl->InstanceTemplate()->SetInternalFieldCount(1);
NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);
constructor.Reset(isolate, tpl->GetFunction());
exports->Set(String::NewFromUtf8(isolate, "MyObject"), tpl->GetFunction());
}
void MyObject::New(const FunctionCallbackInfo<Value>& args) {
double value = args[0]->IsUndefined() ? 0 : args[0]->NumberValue();
MyObject* obj = new MyObject(value);
obj->Wrap(args.This());
args.GetReturnValue().Set(args.This());
}
void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.Holder());
obj->_value += 1;
args.GetReturnValue().Set(Number::New(isolate, obj->_value));
}
NODE_MODULE(addon, MyObject::Init)
複製代碼
修改binding.gyp文件
{
"targets": [
{
"target_name": "addon",
"sources": [ "myobject.cc" ]
}
]
}
複製代碼
經過node-gyp build編譯成功以後會在build/Release/目錄下生成文件addon.node。這樣咱們就能夠在JavaScript中使用MyObject了:
const addon = require('./addon');
let obj = new addon.MyObject();
console.log(obj.plusOne());
console.log(obj.plusOne());
console.log(obj.plusOne());
let obj1 = new addon.MyObject(10);
console.log(obj1.plusOne());
複製代碼
執行結果以下:
雖然Node.js的啓動過程很簡潔,但仍是有一些問題能夠繼續深挖。好比,一個網絡請求在Node.js中究竟是怎麼被處理的呢?但願本文能夠拋磚引玉,在入門階段給你們一點幫助。
iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、噹噹開售。
2019年,iKcamp原創新書《Koa與Node.js開發實戰》已在京東、天貓、亞馬遜、噹噹開售啦!