1.先从Nodejs的入口谈起,以下采用的是nodejs 6.9.4 版本。
nodejs启动的入口文件src/node_main.cc
// UNIX
int main(int argc, char *argv[]) {
// Disable stdio buffering, it interacts poorly with printf()
// calls elsewhere in the program (e.g., any logging from V8.)
setvbuf(stdout, nullptr, _IONBF, 0);
setvbuf(stderr, nullptr, _IONBF, 0);
return node::Start(argc, argv);
}
由代码可看出,主要运行的是node::Start方法,此方法定义在src/node.cc文件中。
int Start(int argc, char** argv) {
PlatformInit();
CHECK_GT(argc, 0);
// Hack around with the argv pointer. Used for process.title = "blah".
argv = uv_setup_args(argc, argv);
// This needs to run *before* V8::Initialize(). The const_cast is not
// optional, in case you're wondering.
int exec_argc;
const char** exec_argv;
Init(&argc, const_cast<const char**>(argv), &exec_argc, &exec_argv);
#if HAVE_OPENSSL
#ifdef NODE_FIPS_MODE
// In the case of FIPS builds we should make sure
// the random source is properly initialized first.
OPENSSL_init();
#endif // NODE_FIPS_MODE
// V8 on Windows doesn't have a good source of entropy. Seed it from
// OpenSSL's pool.
V8::SetEntropySource(crypto::EntropySource);
#endif
v8_platform.Initialize(v8_thread_pool_size);
V8::Initialize();
int exit_code = 1;
{
NodeInstanceData instance_data(NodeInstanceType::MAIN,
uv_default_loop(),
argc,
const_cast<const char**>(argv),
exec_argc,
exec_argv,
use_debug_agent);
StartNodeInstance(&instance_data);
exit_code = instance_data.exit_code();
}
V8::Dispose();
v8_platform.Dispose();
delete[] exec_argv;
exec_argv = nullptr;
return exit_code;
}
此方法末尾处调用了同一个文件中的StartNodeInstance方法,传入一个NodeInstanceData(node_internals.h中定义)实例,在这个方法中调用了LoadEnvironment方法,LoadEnvironment方法有如下代码:
// Execute the lib/internal/bootstrap_node.js file which was included as a
// static C string in node_natives.h by node_js2c.
// 'internal_bootstrap_node_native' is the string containing that source code.
Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(),
"bootstrap_node.js");
Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);
if (try_catch.HasCaught()) {
ReportException(env, try_catch);
exit(10);
}
// The bootstrap_node.js file returns a function 'f'
CHECK(f_value->IsFunction());
Local<Function> f = Local<Function>::Cast(f_value);
// Add a reference to the global object
Local<Object> global = env->context()->Global();
#if defined HAVE_DTRACE || defined HAVE_ETW
InitDTrace(env, global);
#endif
#if defined HAVE_LTTNG
InitLTTNG(env, global);
#endif
#if defined HAVE_PERFCTR
InitPerfCounters(env, global);
#endif
// Enable handling of uncaught exceptions
// (FatalException(), break on uncaught exception in debugger)
//
// This is not strictly necessary since it's almost impossible
// to attach the debugger fast enought to break on exception
// thrown during process startup.
try_catch.SetVerbose(true);
env->SetMethod(env->process_object(), "_rawDebug", RawDebug);
// Expose the global object as a property on itself
// (Allows you to set stuff on `global` from anywhere in JavaScript.)
global->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "global"), global);
// Now we call 'f' with the 'process' variable that we've built up with
// all our bindings. Inside bootstrap_node.js and internal/process we'll
// take care of assigning things to their places.
// We start the process this way in order to be more modular. Developers
// who do not like how bootstrap_node.js sets up the module system but do
// like Node's I/O bindings may want to replace 'f' with their own function.
Local<Value> arg = env->process_object();
f->Call(Null(env->isolate()), 1, &arg);
可以看到,这个node通过执行/lib/internal/bootstrap_node.js进行初始化, ExecuteString之后得到的是bootstrap_node的一个对象,将其类型转换成Function,在最后执行之f->Call(Null(env->isolate()), 1, &arg);执行的时候传入的&arg就是process对象的引用。而process对象的设置是在相同文件的SetupProcessObject函数中设置的,在这个调用过程中在process对象上设置了除bind和dlopen外的几乎所有方法和对象。bootstrap_node.js执行初始化的就是Nodejs的module模块系统。
2.Node启动后会首先加载其核心模块(既纯用C++编写的Nodejs自带模块)。
而在js中引用这些模块都是使用process.binding函数进行引用,在process的初始化函数SetupProcessObject中可以看到如下代码:
env->SetMethod(process, "binding", Binding);
也就是说实际使用的是node.cc文件中的Binding函数,如下:
node_module* mod = get_builtin_module(*module_v);
if (mod != nullptr) {
exports = Object::New(env->isolate());
// Internal bindings don't have a "module" object, only exports.
CHECK_EQ(mod->nm_register_func, nullptr);
CHECK_NE(mod->nm_context_register_func, nullptr);
Local<Value> unused = Undefined(env->isolate());
mod->nm_context_register_func(exports, unused,
env->context(), mod->nm_priv);
cache->Set(module, exports);
} else if (!strcmp(*module_v, "constants")) {
exports = Object::New(env->isolate());
DefineConstants(env->isolate(), exports);
cache->Set(module, exports);
} else if (!strcmp(*module_v, "natives")) {
exports = Object::New(env->isolate());
DefineJavaScript(env, exports);
cache->Set(module, exports);
} else {
char errmsg[1024];
snprintf(errmsg,
sizeof(errmsg),
"No such module: %s",
*module_v);
return env->ThrowError(errmsg);
}
args.GetReturnValue().Set(exports);
可以看到,是调用get_builtin_module函数来获取编译好的c++模块的,看下get_builtin_module函数:
struct node_module* get_builtin_module(const char* name) {
struct node_module* mp;
for (mp = modlist_builtin; mp != nullptr; mp = mp->nm_link) {
if (strcmp(mp->nm_modname, name) == 0)
break;
}
CHECK(mp == nullptr || (mp->nm_flags & NM_F_BUILTIN) != 0);
return (mp);
}
可以看到,这个函数在modlist_builtin结构中寻找对应输入名称的模块并返回。modlist_builtin结构是在node_module_register函数(在node.cc文件中被定义)调用时被逐条注册的,而node_module_register调用的地方是在node.h中的宏定义
#define NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, priv, flags) \
extern "C" { \
static node::node_module _module = \
{ \
NODE_MODULE_VERSION, \
flags, \
NULL, \
__FILE__, \
NULL, \
(node::addon_context_register_func) (regfunc), \
NODE_STRINGIFY(modname), \
priv, \
NULL \
}; \
NODE_C_CTOR(_register_ ## modname) { \
node_module_register(&_module); \
} \
}
#define NODE_MODULE_CONTEXT_AWARE_BUILTIN(modname, regfunc) \
NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, NULL, NM_F_BUILTIN) \
在每个src目录下的c++模块都会在结尾处调用NODE_MODULE_CONTEXT_AWARE_BUILTIN,也就是在模块被引入后就会将模块注册到modlist_builtin中去。供后续被其他模块引用。
3.接下来让我们看一下js环境初始化(bootstrap.js)中执行了什么。
从bootstrap.js文件整体是一个匿名函数,入参是process。此匿名函数执行后会调用其内部定义的startup函数。做的事情基本是先通过NativeModule对象的require方法引入各种Node的内置模块(lib目录下的js文件)进行初始化,之后通过process的第二个入参argv[1]即node进程启动时所带的参数来确定是主的js进程还是child进程。主进程通过run(Module.runMain);
启动业务代码,子进程通过process的stdin读入code,再通过evalScript('[stdin]');
去执行。
下面我们首先看下NativeMudule对象,
function NativeModule(id) {
this.filename = `${id}.js`;
this.id = id;
this.exports = {};
this.loaded = false;
this.loading = false;
}
NativeModule._source = process.binding('natives');
NativeModule._cache = {};
NativeModule.require = function(id) {
if (id === 'native_module') {
return NativeModule;
}
const cached = NativeModule.getCached(id);
if (cached && (cached.loaded || cached.loading)) {
return cached.exports;
}
if (!NativeModule.exists(id)) {
throw new Error(`No such native module ${id}`);
}
process.moduleLoadList.push(`NativeModule ${id}`);
const nativeModule = new NativeModule(id);
nativeModule.cache();
nativeModule.compile();
return nativeModule.exports;
};
可以看到,NativeModule是一个构造函数,其上绑定了一个静态方法require
,一个新的模块被require后,会在process的moduleLoadList
中注册之后实例化一个NativeModule
实例,并调用该实例的cache
和compile
方法,最后返回NativeModule
实例的exports
属性。
先看cache函数:
NativeModule.prototype.cache = function() {
NativeModule._cache[this.id] = this;
};
再看compile函数:
NativeModule.prototype.compile = function() {
var source = NativeModule.getSource(this.id);
source = NativeModule.wrap(source);
this.loading = true;
try {
const fn = runInThisContext(source, {
filename: this.filename,
lineOffset: 0,
displayErrors: true
});
fn(this.exports, NativeModule.require, this, this.filename);
this.loaded = true;
} finally {
this.loading = false;
}
};
compile函数先调用 NativeModule的静态方法getSource获取对应内置模块代码,再通过wrap进行包装,再使用runInThisContext编译返回一个可执行函数,最后执行之。
1.getSource方法如下:
NativeModule.getSource = function(id) {
return NativeModule._source[id];
};
结合上面的代码可知,NativeModule的代码是通过process.binding('natives');
获取的。
再回到Binding源码中,
} else if (!strcmp(*module_v, "natives")) {
exports = Object::New(env->isolate());
DefineJavaScript(env, exports);
cache->Set(module, exports);
} else {
process.binding('natives');
返回的是DefineJavaScript
函数执行的结果,DefineJavaScript
在node_javascript.cc中定义。
void DefineJavaScript(Environment* env, Local<Object> target) {
HandleScope scope(env->isolate());
for (auto native : natives) {
if (native.source != internal_bootstrap_node_native) {
Local<String> name = String::NewFromUtf8(env->isolate(), native.name);
Local<String> source =
String::NewFromUtf8(
env->isolate(), reinterpret_cast<const char*>(native.source),
NewStringType::kNormal, native.source_len).ToLocalChecked();
target->Set(name, source);
}
}
}
可以看到,函数遍历的natives
数组,也就是说natives
数组中定一个了node内置模块的名称和代码。而natives
定义的位置则比较特别,它是在编译时被动态写入到node_natives.h文件中的。这时候就需要看工程最外层的node.gyp
文件了
...
'library_files': [
'lib/internal/bootstrap_node.js',
'lib/_debug_agent.js',
'lib/_debugger.js',
'lib/assert.js',
'lib/buffer.js',
'lib/child_process.js',
'lib/console.js',
'lib/constants.js',
'lib/crypto.js',
...
]
...
{
'target_name': 'node_js2c',
'type': 'none',
'toolsets': ['host'],
'actions': [
{
'action_name': 'node_js2c',
'inputs': [
'<@(library_files)',
'./config.gypi',
],
'outputs': [
'<(SHARED_INTERMEDIATE_DIR)/node_natives.h',
],
'conditions': [
[ 'node_use_dtrace=="false" and node_use_etw=="false"', {
'inputs': [ 'src/notrace_macros.py' ]
}],
['node_use_lttng=="false"', {
'inputs': [ 'src/nolttng_macros.py' ]
}],
[ 'node_use_perfctr=="false"', {
'inputs': [ 'src/perfctr_macros.py' ]
}]
],
'action': [
'python',
'tools/js2c.py',
'<@(_outputs)',
'<@(_inputs)',
],
},
],
}, # end node_js2c
可以看到,这个配置项是启动python执行tools/js2c.py文件,输入是library_files
即lib目录下的内置模块,输出是src/node_natives.h文件。具体js2c.py脚本不细述了,基本思路是按照c++头文件格式,将js内置模块写入到这样的一个结构中。
struct _native {
const char* name;
const unsigned char* source;
size_t source_len;
};
这样js内置模块在node启动后就可被引用了。
3.对于文件模块的加载
1.对于文件模块和第三方模块,其他文件调用require时,即上文中boostrap.js中提到的require函数,首先会查看内存中是否已经加载了该模块,如果有就直接返回内存中的该模块。
所以模块在同一个node进程中只会加载一次,也就是说1.模块中的立即执行代码只会执行一次,2.因为在内存中是同一个对象,所以对该模块内状态的修改会影响到所有引入该模块的代码逻辑。
2.对于文件模块和第三方模块的加载工作,整个过程要进过”路径分析“、”模块定位“、”后缀分析“。
这几个过程会影响模块加载速度,如
a.”路径分析“:文件模块使用相对或绝对路径,寻址较快,第三方模块需要从当前调用文件的目录的node_module开始逐层向上查找,寻址较慢。
b."后缀分析",对无后缀的文件标识,引擎会自动补全js、json、node后缀;从js开始试起,所以没事儿还是加好后缀比较好,尤其node后缀。
3.获取到完整文件后,会以utf8格式同步读入文件,然后执行
var wrapper = Module.wrap(content);
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
});
// ...
var result = compiledWrapper.call(this.exports, this.exports, require, this,
filename, dirname);
其中content就是读入的字节流,重点是wrap函数,这个函数是在字节流前后注入字符,使之变成如下形式:
'(function (exports, require, module, __filename, __dirname) { /* content */\n});'
其中exports、module、require这三个在文件中调用的对象就是在这里注入的。
综述:node的模块是对commonjs的具体实现,文件即模块,通过exports导出,require引入,模块可以根据逻辑按需加载,但加载过程是阻塞型的,即加载完成后才会继续执行后续逻辑代码
这部分可参考:https://zhuanlan.zhihu.com/p/376689147
https://zhuanlan.zhihu.com/p/25916585
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。