乾貨 | 走進Node.js之啓動過程剖析

走進Node.js之啓動過程剖析

做者:正龍 (滬江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就是作模塊查找,其執行過程以下:

  1. 從Args中獲取到模塊名稱。
  2. 從Binding Cache中看是否能找到模塊,若是有直接返回模塊的exports。
  3. 3往Module Load List中追加一條模塊記錄,名稱爲"binding " + 模塊名。
  4. 調用get_builtin_module,參數是模塊名,get_builtin_module會從modlist_builtin列表中查找內置模塊,全部內置模塊和第三方擴展都記錄在modlist_builtin列表中。C/C++模塊經過NODE_MODULE_CONTEXT_AWARE_BUILTIN註冊,第三方擴展模塊經過NODE_MODULE註冊。最終都會調用node_module_register。node_module結構體包含註冊函數、模塊名稱、文件名稱等信息。
  5. 若是查找到,則返回對應模塊的exports。
  6. 若是模塊名是constants,則調用DefineContstants。
  7. 若是模塊名是natives,則調用DefineJavaScript,會返回全部內置模塊,它們通常由Javascript實現。這些模塊在/lib目錄下,會經過js2c.py轉成c代碼,js2c.py會生成一個臨時文件node_natives.h,裏面包含了NODE_NATIVES_MAP的定義。
  8. 不然,拋出錯誤:沒有指定名稱的模塊。

環境對象準備好以後,就開始真正加載Node.js自身提供的JavaScript類庫代碼。LoadEnvironment執行過程以下:

  1. 調用ExecuteString執行bootstrap_node.js。bootstrap_node.js文件裏定義了一個函數它會往Global對象上添加屬性,經過internal/module加載Node.js自身提供的JavaScript類庫。
  2. 執行上一步返回的函數,並傳入env->process_object()對象。

到這兒,咱們能夠總結2個問題:

  1. Node.js裏面本身提供的JavaScript庫是怎麼實現的?

    經過C/C++代碼封裝成Node.js內置模塊,而後再經過process.binding暴露給JavaScript。

  2. 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開發實戰》已在京東、天貓、亞馬遜、噹噹開售啦!

相關文章
相關標籤/搜索