当我们运行node app.js
的时候都发生了什么?process
的初始化,模块系统的形成,C/C++与js的结合等等。从源头出发,探索背后的奥秘。
启动及初始化操作
node_main.cc
是node的入口,根据操作系统做一些初始化工作,最后调用node::Start()
在node.cc
里定义了Start()
,做了一些初始化platform,V8初始化,libuv event loop创建等工作,然后调用第一个inline Start()
:1
2const int exit_code =
Start(uv_default_loop(), argc, argv, exec_argc, exec_argv);
在在第一个inline Start()
里,起一个V8实例,并调用最后一个inline Start()
:
1 | Isolate* const isolate = Isolate::New(params); |
接着在最后一个Start()里,初始化context,新建一个env,env用于将libuv
和v8
结合在一起:1
2
3
4
5
6HandleScope handle_scope(isolate);
Local<Context> context = Context::New(isolate);
Context::Scope context_scope(context);
Environment env(isolate_data, context);
...
env.Start(argc, argv, exec_argc, exec_argv, v8_is_profiling);
这里调用了env.Start();
,env.Start()
定义在env.cc
里,该方法里面调用了SetupProcessObject(this, argc, argv, exec_argc, exec_argv);
,而该方法又定义在node.cc
里,定义了process
的一些属性和方法(其中包括了process.binding()
用于C/C++模块机制,后面详解):
最后一个inline Start()
还进入了一个while循环处理libuv
事件:
1 | do { |
另外还调用了LoadEnvironment(&env);
:
1 | Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(), |
其中MainSource(env)
:
1 | Local<String> MainSource(Environment* env) { |
而这里的internal_bootstrap_node_native
由node_natives.h
定义,这个头文件是由js2c.py
工具生成的,将所有native模块都编译到C++数组里:
执行bootstrap_node.js
的匿名函数并传入process
对象,process
对象通过env->process_object()
获得:
1 | ... |
在bootstrap_node.js
初始化了一些process方法和属性,global变量,模块机制等。
执行一个js文件
为了说明一个运行一个js文件发生了什么,先说明一下模块系统的初始化
模块系统形成
process.binding() C/C++内建模块
上面我们说在程序启动后会在SetupProcessObject()
里为process
对象绑定一些方法,其中就包括process.binding
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17if (cache->Has(env->context(), module).FromJust()) {
exports = cache->Get(module)->ToObject(env->isolate());
args.GetReturnValue().Set(exports);
return;
}
...
node_module* mod = get_builtin_module(*module_v);
if (mod != nullptr) {
exports = Object::New(env->isolate());
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 ...
其中get_builtin_module(*module_v);
在modlist_builtin
链表中获取模块。同样用了缓存机制。那么这些C/C++模块是怎么放到链表上面去的呢?答案是通过NODE_MODULE_CONTEXT_AWARE_BUILTIN
,比如zlib
调用了NODE_MODULE_CONTEXT_AWARE_BUILTIN(zlib, node::InitZlib)
来将该模块加入到上边儿的链表中。
我们在node.h
看到了这个宏定义:1
2
NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, NULL, NM_F_BUILTIN)
而NODE_MODULE_CONTEXT_AWARE_X
最终会调用node.cc
里定义的node_module_register(&_module);
将C/C++模块加入到modlist_builtin
链表中,供get_builtin_module()
使用。
1 | extern "C" void node_module_register(void* m) { |
native js模块
在bootstrap_node.js
里:1
2NativeModule._source = process.binding('natives');
NativeModule._cache = {};
当调用process.binding('natives');
的时候,node.cc
:
1 | if (!strcmp(*module_v, "natives")) { |
在src/node_javascript.cc
中关于DefineJavaScript()
:
1 | void DefineJavaScript(Environment* env, Local<Object> target) { |
而上面的natives
就是在node_natives.h
里边儿定义的:
对于require
同样使用了cache机制:1
2
3
4const cached = NativeModule.getCached(id);
if (cached && (cached.loaded || cached.loading)) {
return cached.exports;
}
最终调用compile()
方法:
对源码用wrapper
进行了包装:1
2var source = NativeModule.getSource(this.id);
source = NativeModule.wrap(source);
然后在vm里执行,并传入一些包装后匿名函数需要的参数:1
2
3
4
5
6const fn = runInThisContext(source, {
filename: this.filename,
lineOffset: 0,
displayErrors: true
});
fn(this.exports, NativeModule.require, this, this.filename);
这样我们就可以来理解执行一个文件的过程:
执行一个js文件(文件模块)
1 | const path = NativeModule.require('path'); |
同步读取执行的js文件,lib/module.js
中的runMain()
:1
2
3
4Module.runMain = function() {
Module._load(process.argv[1], null, true);
process._tickCallback();
};
Module._load:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30Module._load = function(request, parent, isMain) {
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
}
var filename = Module._resolveFilename(request, parent, isMain);
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
if (NativeModule.nonInternalExists(filename)) {
debug('load native module %s', request);
return NativeModule.require(filename);
} //如果在native模块里找到就调用NativeModule的require机制
var module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
Module._cache[filename] = module;
tryModuleLoad(module, filename);
return module.exports;
};
Module._resolveFilename()
经过一系列的查找机制(包括后缀扩展,包查找等)后,得到一个合适的filename
,tryModuleLoad()
里会调用Module.load()
:1
2
3
4var extension = path.extname(filename) || '.js';
if (!Module._extensions[extension]) extension = '.js';
Module._extensions[extension](this, filename);
this.loaded = true;
对于js文件调用_compile()
方法:
同样进行了包装(包装方法和内容和NativeModule相同),并传入自己的参数在vm里执行代码:1
2
3
4
5
6
7
8
9
10content = internalModule.stripShebang(content);
var wrapper = Module.wrap(content);
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
});
...
result = inspectorWrapper(compiledWrapper, this.exports, this.exports,
require, this, filename, dirname);
而传入的require
就是传入的Module.prototype.require()
:1
2
3
4
5Module.prototype.require = function(path) {
assert(path, 'missing path');
assert(typeof path === 'string', 'path must be a string');
return Module._load(path, this, /* isMain */ false);
};
可见最终又是走_load
。这其实是文件模块(第三方和自定义模块)的加载方式,而用node执行一个js文件,实际上用到的也就是这种文件模块的机制,不过多了一系列的启动操作。
可以分析得到,执行一个js文件时,会去初始化process
,其中包括定义了process.binding()
方法来定义C/C++
模块机制,然后会去执行一个native模块即bootstrap_node.js
,它的代码放在了node_natives.h
里,从那里读取code array,在C++层面运行即调用了bootstrap_node.js
的匿名函数并传入process
对象,在bootstrap_node.js
里,定义了native js模块机制,即通过process.binding('natives)
得到node_natives.h
里的natives
数组,包含了所有native模块的代码数组。然后对于执行一个js文件,调用原生模块module
,去执行Module.runMain()
,而这个操作不过是由module
定义的文件模块机制罢了。
总结
文章从node启动到一个js文件的执行的角度去分析内部原理,详细解释了与process
对象有关和模块系统的形成。而对于其他细节诸如libuv event loop
机制还需要深究,会在后面的文章中进行总结。欢迎讨论。