做者:正龙 (沪江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,意思是从队列中只取一个任务执行,无论队列中当前是否有多个任务。segmentfault
到这儿,大概能够理解到Node.js的“单线程”是怎么回事。那运行的Node.js进程确实只开启了一个线程吗?咱们打开任务管理器看看:api
实际上,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前端高效开发实战》已在亚马逊、京东、当当开售。