作者:正龙(沪江Web前端开发工程师)本文为原创,转载请注明作者和出处。随着Node.js的流行,越来越多的开发者使用Node.js搭建环境,很多公司也开始将网站迁移到Node.js服务器上。Node.js的优势显而易见,本文不再赘述,那么它是如何做到的呢?内在逻辑是什么?带着这些疑问,笔者开始了漫长的Node.js研究之旅。今天,笔者就来和大家一起探讨一下Node.js的起步原理。Node.js主要依赖谷歌的V8引擎和libuv实现。V8,想必大家都不陌生,率先将JavaScript直接翻译成汇编代码执行,让很多不可能的事情成为可能,比如Node.js。libuv是一个跨平台的异步IO库。它所说的IO不仅包括本地文件操作,还包括TCP、UDP等网络socket操作。范围甚至可以扩展到所有的流操作(Stream)。因此,我们可以将Node.js理解为添加了网络功能的V8。为方便描述,以下环境均基于Windows7专业版。使用MAC的朋友不要惊慌,内容本质还是适用的,具体条款可能略有不同。另外小伙伴可以下载一个Node.js的源码(点此下载),本文使用6.10.0LTS。我们打开Node.js的二进制发布包,内容很简单:node.exe、npm和node.h头文件。node.h头文件仅在开发Node.js插件时使用。当我们启动node.exe时,它到底做了什么?首先,它是一个EXE可执行文件,所以必须有一个main函数。Node.js的main函数定义在node_main.cc中,主要是初始化V8Platform和v8engine;然后启动一个Node.js实例。具体调用环节如图:Init函数主要是解析Node.js的启动参数,过滤V8选项给JavaScript引擎。Node.js的main函数很短,运行和返回应该很快。其实命令行窗口会一直等待,并没有立即退出,这是怎么回事?答案就在StartInstance。首先,它会创建一个V8执行沙箱,生成并初始化Node.js运行环境对象,然后开始Node.js循环等待。具体如图所示:也就是说,Node.js的主线程主要消费来自UV默认事件循环(uv_default_loop)和V8的MainThreadQueue和MainThreadDelayedQueue的任务。uv_run是一个阻塞调用。如果队列中有任务,则执行并返回true,否则阻塞当前线程;如果返回false,整个Node.js进程都会释放资源退出。注意参数UV_RUN_ONCE,表示只从队列中取出一个任务执行,而不管队列中当前是否有多个任务。至此,你大概可以理解什么是Node.js的“单线程”了。正在运行的Node.js进程真的只启动一个线程吗?我们打开任务管理器看看:其实Node.js进程目前有7个线程。查阅文档后发现Node.js可以通过指定参数--v8-pool-size来设置V8线程池的大小。原来V8的字节码编译、优化、GC都是通过多线程完成的;进一步排查,发现环境变量UV_THREADPOOL_SIZE会影响libuv的线程池大小。到目前为止,Node.js所做的工作可以概括为初始化V8和libuv。接下来我们看看Node.js自身的运行环境是如何搭建的。Node.js本身的运行环境由Environment类表示,我们需要构建进程对象。进程对象可在JavaScript应用程序代码中访问,其文档可在此处找到。请注意,该进程尚未分配给Global对象。CreateEnvironment的执行流程如图:调用setAutorunMicrotask禁止V8消费队列中的任务。SetupProcessObject主要是设置进程的属性,比如比较重要的binding,以及其他提供给开发者的字段,比如cpuUsage,hrtime,uptime等。Binding用于获取C/C++构建的模块,net库中的Node.js就是这样最终调用到libuv的。Binding就是做模块查找,其执行过程如下:从Args中获取模块名。检查是否可以从BindingCache中找到该模块,如果有直接返回该模块的exports。3在ModuleLoadList中添加一条模块记录,命名为“binding”+模块名。调用get_builtin_module,参数为模块名,get_builtin_module会从modlist_builtin列表中寻找内置模块,所有内置模块和第三方扩展都记录在modlist_builtin列表中。C/C++模块通过NODE_MODULE_CONTEXT_AWARE_BUILTIN注册,第三方扩展模块通过NODE_MODULE注册。最终会调用node_module_register。node_module结构包含注册函数、模块名、文件名等信息。如果找到,则返回相应模块的导出。如果模块名称是常量,则调用DefineContstants。如果模块名是natives,调用DefineJavaScript会返回所有的内置模块,一般都是用Javascript实现的。/lib目录下的这些模块会通过js2c.py转换成c代码,js2c.py会生成一个临时文件node_natives.h,里面包含了NODE_NATIVES_MAP的定义。否则,将抛出错误:没有具有指定名称的模块。环境对象准备好后,开始真正加载Node.js自身提供的JavaScript类库代码。LoadEnvironment的执行过程如下:调用ExecuteString执行bootstrap_node.js。bootstrap_node.js文件定义了一个函数,为Global对象添加属性,并通过internal/module加载Node.js自身提供的JavaScript类库。执行上一步返回的函数,传入env->process_object()对象。至此,我们可以总结出两个问题: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,返回当前值。namespacedemo{classMyObject:publicnode::ObjectWrap{public:staticvoidInit(v8::Local
