该文章当前使用的nodejs版本是v13.1.0(网上那些分析nodejs源码的文章不写清基于的版本都是耍流氓),很是干货的一篇文章,请耐心阅读,不然建议收藏javascript
阅读本篇文章以前请先阅读前置文章:html
读完本篇文章你会掌握:java
首先,nodejs提供那么多模块,以及能在各个平台上跑的飞起,不是由于js很牛逼,而是由于底层依赖了一些你不知道的技术。最大的两个依赖即是v8和libuv。为何这么说呢?由于一个帮助你将js代码转变成能够在各个平台和机器上运行的机器码,另一个帮助你调用平台和机器上各类系统特性,包括操做文件、监听socket等等。先撇开这两个最大的依赖,咱们看一下nodejs源码中的deps
目录都有些啥?node
上图即是Nodejs依赖的包,在官网咱们能够找到里面一些依赖包的介绍:Dependenciespython
resolve()
族函数。其余几个没在官网提到的这里也说一下:webpack
node debug
命令。由于是要面向Javascript开发人员,因此咱们不可能直接上来就写C++/C代码,那么确定须要一个东西去封装这些C++/C代码,并提供一套优雅的接口给开发者,因而Nodejs就是干这事的。一言以蔽之:git
Nodejs封装了全部与底层交流的信息,给开发者提供一致的接口定义。在不断升级v8和libuv的同时,依然可以作到接口的一致性,这个就是nodejs想要实现的目标。
复制代码
那么问题来了,nodejs究竟是怎么将libuv和v8封装起来并提供接口的?搞懂这一切以前,咱们先看看Nodejs的目录结构,这个目录结构在后面的讲解中有用到:github
nodejs源码有两个重要的目录:web
lib
: 包含了全部nodejs函数和模块的javascript实现,这些实现都是能够直接在你js项目中引用进去的面试
src
: 包含了全部函数的C++版本实现,这里的代码才会真正引用Libuv和V8。
而后咱们随便查看一个lib目录下的文件能够看到,除了正常的js语法以外,出现了一个在平时应用程序没有见到的方法:internalBinding
。这个是啥?有啥做用?
咱们的探索之旅即是从这个方法开始,一步步深刻到nodejs内部,一步步带你们揭开nodejs的神秘面纱。首先咱们要从nodejs的编译过程提及。
再讲编译过程以前,咱们还得普及一下Nodejs源码内部的模块分类和C++加载绑定器两个概念。
nodejs模块能够分为下面三类:
http
,fs
等等好比lib
目录下的fs.js
就是native模块,而fs.js
调用的src
目录下的node_fs.cc
就是内建模块。知道了模块的分类,那么好奇这些模块是怎么加载进来的呢?(本文非讲解模块加载的,因此第三方模块不在讨论范围内)
后面会有文字涉及到这几个概念:
NODE_BUILTIN_MODULE_CONTEXT_AWARE()
来建立,而且它们的nm_flags都设置为NM_F_BUILTIN
NODE_MODULE_CONTEXT_AWARE_CPP()
宏来建立,其flag设置为NM_F_LINKED
NODE_MODULE_CONTEXT_AWARE_INTERNAL()
宏来建立,其flag设置为NM_F_INTERNAL
根据官网的推荐,源码编译简单粗暴:
$ ./configure
$ make -j4
复制代码
咱们能够从nodejs编译配置文件中提取出一些重要信息。
众所周知,Nodejs使用了GYP的编译方式,其GYP编译文件是:node.gyp
,咱们从该文件的两处地方获取到两个重要的信息。
从该文件的target
字段能够看到,编译以后会生成多个target,可是最重要的是第一个target
,其配置:
{ // 定义的'node_core_target_name%'就是'node', 'target_name': '<(node_core_target_name)', 'type': 'executable', // 这里的类型是可执行文件 'defines': [ 'NODE_WANT_INTERNALS=1', ], 'includes': [ 'node.gypi' ], 'include_dirs': [ 'src', 'deps/v8/include' ], 'sources': [ 'src/node_main.cc' ], ... ... } 复制代码
由此可知,整个node应用程序的入口文件其实就是node_main.cc
。
编译文件的第二个target是libnode
,它是将其他剩余的C++文件编译成库文件,可是有一个特殊的地方就是该target在编译以前有个action:
{ // 这里定义的'node_lib_target_name'就是libnode 'target_name': '<(node_lib_target_name)', 'type': '<(node_intermediate_lib_type)', 'includes': [ 'node.gypi', ], 'include_dirs': [ 'src', '<(SHARED_INTERMEDIATE_DIR)' # for node_natives.h ], ... ... 'actions': [ { 'action_name': 'node_js2c', 'process_outputs_as_sources': 1, 'inputs': [ # Put the code first so it's a dependency and can be used for invocation. 'tools/js2c.py', '<@(library_files)', 'config.gypi', 'tools/js2c_macros/check_macros.py' ], 'outputs': [ '<(SHARED_INTERMEDIATE_DIR)/node_javascript.cc', ], 'conditions': [ [ 'node_use_dtrace=="false" and node_use_etw=="false"', { 'inputs': [ 'tools/js2c_macros/notrace_macros.py' ] }], [ 'node_debug_lib=="false"', { 'inputs': [ 'tools/js2c_macros/nodcheck_macros.py' ] }], [ 'node_debug_lib=="true"', { 'inputs': [ 'tools/js2c_macros/dcheck_macros.py' ] }] ], 'action': [ 'python', '<@(_inputs)', '--target', '<@(_outputs)', ], }, ], 复制代码
从这个配置信息来看是说有个js2c.py
的python文件会将lib/**/*.js
和deps/**/*.js
的全部js文件按照其ASCII码转化为一个个数组放到node_javascript.cc
文件中。
生成的node_javascript.cc
文件内容大体以下:
namespace node { namespace native_module { ... static const uint8_t fs_raw[] = {...} ... void NativeModuleLoader::LoadJavaScriptSource() { ... source_.emplace("fs", UnionBytes{fs_raw, 50659}); ... } UnionBytes NativeModuleLoader::GetConfig() { return UnionBytes(config_raw, 3017); // config.gypi } } 复制代码
这种作法直接就将js文件全都缓存到内存,避免了多余的I/O操做,提升了效率。
所以从上述配置信息咱们能够总结出这样一张编译过程:
好了,清楚了编译流程以后,咱们再从nodejs的启动过程来分析internalBinding
究竟是何方神圣。
上一小节咱们知道nodejs应用程序的入口文件是node_main.cc
,因而咱们从这个文件开始追踪代码,获得如下一个流程图:
其中标注红色的是须要关注的重点,里面有些知识和以前的那些文章能够联系起来,若是你阅读过耗时两个月,网上最全的原创nodejs深刻系列文章(长达十来万字的文章,欢迎收藏)中列举的一些基础文章,看到这里,相信有种恍然大悟的感受,感受知识点一会儿均可以联系起来了,这就是系统学习的魅力~
回到上图,全部的线索都聚焦到了这个函数中:NativeModuleLoader::LookupAndCompile
。在调用这个函数以前,还有一个重点就是:此时NativeModuleLoader是实例化的,因此其构造函数是被执行掉的,而其构造函数执行的只有一个函数:LoadJavaScriptSource(),该函数就是上一小节咱们看到node_javascript.cc文件中的函数,因而咱们有如下结论:
internal/bootstrap/loader.js
是咱们执行的第一个js文件那么NativeModuleLoader::LookupAndCompile
都作了些什么呢?
NativeModuleLoader::LookupAndCompile
它利用咱们传入的文件id(此次传递的是internal/bootstrap/loader.js
)在_source
变量中查找,找到以后将整个文件内容包裹起来成为一个新的函数,并追加进一些函数的定义(此次传递的是getLinkedBinding
和getInternalBinding
)以便在js文件中能够调用这些C++函数,而后执行该新函数。这个参数的传递是在上图中的Environment::BootstrapInternalLoaders
函数中:
MaybeLocal<Value> Environment::BootstrapInternalLoaders() { EscapableHandleScope scope(isolate_); // Create binding loaders std::vector<Local<String>> loaders_params = { process_string(), FIXED_ONE_BYTE_STRING(isolate_, "getLinkedBinding"), FIXED_ONE_BYTE_STRING(isolate_, "getInternalBinding"), primordials_string()}; // 这里的GetInternalBinding即是咱们调用`getInternalBinding`执行的函数。若是你不知道为何js能够调用C++函数的话,请参考这篇文章:《如何正确地使用v8嵌入到咱们的C++应用中》 std::vector<Local<Value>> loaders_args = { process_object(), NewFunctionTemplate(binding::GetLinkedBinding) ->GetFunction(context()) .ToLocalChecked(), NewFunctionTemplate(binding::GetInternalBinding) ->GetFunction(context()) .ToLocalChecked(), primordials()}; ... } 复制代码
这个时候加载进loader.js
以后,咱们来看看该文件作了些啥?
internal/bootstrap/loader.js
这个文件很是特殊,是惟一一个没有出现require关键词的js文件,它惟一使用的外部函数就是刚才提到的getLinkedBinding和getInternalBinding,这一点能够经过文件源码进行核实
该文件就是构建出NativeModule
这么一个对象,里面有一些原型方法,最后返回这么一个数据结构:
const loaderExports = {
internalBinding,
NativeModule,
require: nativeModuleRequire
};
复制代码
在里面咱们找到了internalBinding
这个方法的原始实现:
let internalBinding; { const bindingObj = Object.create(null); // eslint-disable-next-line no-global-assign internalBinding = function internalBinding(module) { let mod = bindingObj[module]; if (typeof mod !== 'object') { // 这里调用咱们的C++方法 mod = bindingObj[module] = getInternalBinding(module); moduleLoadList.push(`Internal Binding ${module}`); } return mod; }; } 复制代码
接着咱们顺藤摸瓜,看上图的流程图的一个红色线,loader.js
执行完后的返回值继续传递到了internal/bootstrap/node.js
这个文件使用。
代码以下:
MaybeLocal<Value> Environment::BootstrapInternalLoaders() { ... ... // 这里的loader_exports即是执行完loader.js以后返回的值 Local<Value> loader_exports; if (!ExecuteBootstrapper( this, "internal/bootstrap/loaders", &loaders_params, &loaders_args) .ToLocal(&loader_exports)) { return MaybeLocal<Value>(); } CHECK(loader_exports->IsObject()); Local<Object> loader_exports_obj = loader_exports.As<Object>(); // 此时internal_binding_loader的值即是loader_exports.internalBinding,下面的同理 Local<Value> internal_binding_loader = loader_exports_obj->Get(context(), internal_binding_string()) .ToLocalChecked(); CHECK(internal_binding_loader->IsFunction()); set_internal_binding_loader(internal_binding_loader.As<Function>()); // 注意这里的require是native_module的require,有别于第三方包的reuqire Local<Value> require = loader_exports_obj->Get(context(), require_string()).ToLocalChecked(); CHECK(require->IsFunction()); set_native_module_require(require.As<Function>()); ... } MaybeLocal<Value> Environment::BootstrapNode() { ... ... std::vector<Local<Value>> node_args = { process_object(), native_module_require(), internal_binding_loader(), // 这个就是刚才的那个internalBinding Boolean::New(isolate_, is_main_thread()), Boolean::New(isolate_, owns_process_state()), primordials()}; ... ... } 复制代码
该文件同理,也会注入isMainThread
、ownsProcessState
以及process
、require
、primordials
和internalBinding
六个C++函数供js文件调用。
由此又获得的一个结论就是:
可是到这里,咱们的问题还有一些没有解开,还须要继续深刻。
GetInternalBinding
在internal/bootstrap/node.js
中,大部分都是给process
和global
对象赋值初始化,按照上面给的结论,当咱们调用internalBinding
的时候,实际会执行的是GetInternalBinding
这个C++函数。因此咱们来看看这个函数的实现。
js调用C++函数的规则在如何正确地使用v8嵌入到咱们的C++应用中文章中已经说起过,因此咱们就再也不赘述这个是怎么调用的,咱们关注重点:
void GetInternalBinding(const FunctionCallbackInfo<Value>& args) { ... ... // 查找模块,在哪里查找? node_module* mod = FindModule(modlist_internal, *module_v, NM_F_INTERNAL); if (mod != nullptr) { exports = InitModule(env, mod, module); // 什么是constants模块? } else if (!strcmp(*module_v, "constants")) { exports = Object::New(env->isolate()); CHECK( exports->SetPrototype(env->context(), Null(env->isolate())).FromJust()); DefineConstants(env->isolate(), exports); } else if (!strcmp(*module_v, "natives")) { exports = native_module::NativeModuleEnv::GetSourceObject(env->context()); // Legacy feature: process.binding('natives').config contains stringified // config.gypi CHECK(exports ->Set(env->context(), env->config_string(), native_module::NativeModuleEnv::GetConfigString( env->isolate())) .FromJust()); } else { return ThrowIfNoSuchModule(env, *module_v); } // 这里导出了exports这个变量~ args.GetReturnValue().Set(exports); } 复制代码
这个函数又留给了咱们一些疑问:
constants
和natives
的呢?为了揭开这些问题,咱们继续往下深刻。
NODE_MODULE_CONTEXT_AWARE_INTERNAL
这个时候NODE_MODULE_CONTEXT_AWARE_INTERNAL
隆重登场,细心的童鞋确定发现诸如src/node_fs.cc
这种文件都是以这个宏定义结束的。
在node_binding.h
文件中能够找到其定义:
#define NODE_MODULE_CONTEXT_AWARE_INTERNAL(modname, regfunc) \ NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, nullptr, NM_F_INTERNAL 复制代码
能够看到实际调用的是宏定义NODE_MODULE_CONTEXT_AWARE_CPP
,只是将flag设置为NM_F_INTERNAL
。
而NODE_MODULE_CONTEXT_AWARE_CPP
宏定义则实际上调用了方法:node_module_register
。
node_module_register
这个方法即是往全局的静态变量modlist_internal
和modlist_linked
两个链表挂载模块:
if (mp->nm_flags & NM_F_INTERNAL) { mp->nm_link = modlist_internal; modlist_internal = mp; } else if (!node_is_initialized) { // "Linked" modules are included as part of the node project. // Like builtins they are registered *before* node::Init runs. mp->nm_flags = NM_F_LINKED; mp->nm_link = modlist_linked; modlist_linked = mp; } else { thread_local_modpending = mp; } 复制代码
因而modlist_internal就是一个链表,里面连接着全部内建模块,因此上面的GetInternalBinding
方法是这样的一个执行逻辑:
上图中的那些internalBinding
的调用,提供了各类各样的模块名,其中就有咱们刚才问到constants
和natives
这两个特殊的模块名。
这样,上面的两个问题就迎刃而解了。
可是,问题真的全解决完了吗?若是仅仅是单纯地编译文件的话,这个NODE_MODULE_CONTEXT_AWARE_INTERNAL
是不会被调用的,那么哪里来的调用node_module_register
?
🙆,就欣赏大家这种执着的精神。最后的这个问题,连同整篇文章的一个总结性的流程一块儿释放给你们,算是个大彩蛋~
上图即是一个完整的nodejs和libuv以及v8一块儿合做的流程图,其中有一个点解释了刚才的问题:何时把全部内建模块都加载到modlist_internal
的?答案就是nodejs启动的时候调用binding::RegisterBuiltinModules()
。
至此,按理说整篇文章是能够结束了的,但为了巩固咱们以前的学(zhuang)习(bi),咱们仍是决定以一个例子来看看以前在如何正确地使用v8嵌入到咱们的C++应用中文章中讲的那么多理论,是否是在Nodejs源码中都是对的?
假设有这么一个index.js:
const fs = require('fs') module.exports = () => { fs.open('test.js', () => { // balabala }) } 复制代码
当你在命令行敲入node index.js
回车以后,会有哪些处理流程?
这道题真的太TMD像“当你在浏览器输入某个url回车以后,会通过哪些流程”了。还好,这不是面试(颇有可能会成为面试题哦~)
你们一看也就是两三行代码吗?可是就这么简单的两三行代码,能够出不少面试题哦~好比说:
require
能够不用声明而直接引用?还有好多题目能够问,这里就不一一列举了,想要更多问题欢迎留言(😏)
今天咱们重点不在这些面试题,而是验证C++代码是否是如以前文章写的那样。咱们一行一行解析过去(不会太深刻)。
require('fs')
当你require
的时候,实际上nodejs不直接执行您在js文件中编写的任何代码(除了上面提到的internal/bootstrap/loader.js
和internal/bootstrap/node.js
)。它将您的代码放入一个包装器函数中,而后执行该包装函数。这就是将在任何模块中定义的顶级变量保留在该模块范围内的缘由。
好比:
~ $ node > require('module').wrapper [ '(function (exports, require, module, __filename, __dirname) { ', '\n});' ] > 复制代码
能够看到该包装器函数有5个参数:exports
, require
, module
, __filename
和__dirname
. 因此你在js文件中写的那些require、module.exports其实都是这些形参,而不是真的全局变量
更多细节就不展开了,要不真的就说不完了~
fs.open
open的js文件就不关注了,最终是调用了:
binding.open(pathModule.toNamespacedPath(path),
flagsNumber,
mode,
req);
复制代码
接着咱们跳到node_fs.cc
中,一步步校验以前的理论。
Initialize
还记得上图中那个终极彩蛋里,当调用internalBinding
的时候,是会初始化对应的内建模块,也就是调用其初始化函数,这里即是Initialize
函数。
这个函数一开始即是给target
设置method,好比:
env->SetMethod(target, "close", Close); env->SetMethod(target, "open", Open); 复制代码
那么该方法最后都是调用了that->Set(context, name_string, function).Check();
,这个是否是和咱们在如何正确地使用v8嵌入到咱们的C++应用中中的第二小节二、调用 C++ 函数讲的如出一辙?
接着开始暴露FSReqCallback
这个类,这个在fs.js
文件中有调用到:
const req = new FSReqCallback();
req.oncomplete = callback;
复制代码
那么这个时候咱们就要用到如何正确地使用v8嵌入到咱们的C++应用中中的第三小节三、使用 C++ 类的知识了:
Local<FunctionTemplate> fst = env->NewFunctionTemplate(NewFSReqCallback); fst->InstanceTemplate()->SetInternalFieldCount(1); fst->Inherit(AsyncWrap::GetConstructorTemplate(env)); Local<String> wrapString = FIXED_ONE_BYTE_STRING(isolate, "FSReqCallback"); fst->SetClassName(wrapString); target ->Set(context, wrapString, fst->GetFunction(env->context()).ToLocalChecked()) .Check(); 复制代码
完美契合了以前讲的那些理论知识。
接着咱们看看是如何使用libuv的
Open
异步调用统一封装了一个叫作AsyncCall
的函数,它又调用了AsyncDestCall
:
AsyncCall(env, req_wrap_async, args, "open", UTF8, AfterInteger, uv_fs_open, *path, flags, mode); 复制代码
以后的调用依旧按照咱们以前在fs.c提供的示例同样,只是为了封装,将不少东西隐藏起来,阅读起来比较费劲。
到这里,💐你完成了本篇文章的阅读,也感谢你的耐心让你又掌握了一块知识,还没读懂的话,点个收藏,之后遇到的时候能够拿出来参考参考~
感恩~