前言
之前用过一段时间的v8
,也只是会初始化那个流程,最近想深入了解一下,所以想要通过学习 nodejs
来加深理解。这篇文章主要是讲讲 nodejs
的初始化流程,如有错误,烦请指教~。(本文分析基于 v10.9.0,本文会尽量避免大段源码,但是为了有理有据,还是会放上一些精简过并带有注释的代码上来)。
Helloworld 镇楼:
const http = require('http');
const hostname = '127.0.0.1';
const port = 8888;
http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World\n');
}).listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
写过 nodejs 的都能看懂如上代码。寥寥数行,就创建了一个 http 服务。第一行代码,就出现了一个 require 关键字,那么 require 是从何而来呢?带着这个问题,我们一起去看下吧。
启动流程
1. node 的目录结构,此处就不再分析了。最重要的就是 src 和 lib 了。 src 路径下是 node 的 C++ 实现的主要源码目录,而 lib 主要是 JavaScript 实现所在目录。稍微有一些 C++ 编程基础的同学应该知道,C++ 的启动函数就是 main 函数。那么 node 的启动函数在哪呢。通过全文搜索,可以确定,启动函数就在 src/node_main.cc 这个文件当中了。此处截取部分源码:
// windows 启动方法。
int wmain(int argc, wchar_t* wargv[]) {
//...
// 启动方法。
return node::Start(argc, argv);
}
//...
// 类linux 启动方法。
int main(int argc, char* argv[]) {
// ...
// 启动方法。
return node::Start(argc, argv);
}
可以看到,这个只是一个外壳,做了一些逻辑判断,最终的核心就是调用 Start 方法。
2. Start 方法位于 src/node.cc:
int Start(int argc, char** argv) {
//...
Init(&argc, const_cast<const char**>(argv), &exec_argc, &exec_argv); // 1.
// v8 初始化。
InitializeV8Platform(per_process_opts->v8_thread_pool_size);
v8_initialized = true;
// 开始事件循环。
const int exit_code =
Start(uv_default_loop(), args, exec_args); // 2.
//... v8 开始销毁。
v8_initialized = false;
V8::Dispose();
//...
return exit_code;
}
可以看到,Start 方法主要是执行了一个 Init 方法以及对 v8 进行了初始化的操作,然后开启了整个事件循环流程。
2.1 来看看 Init 方法做了些什么事情,同样位于 src/node.cc 中:
void Init(int* argc,
const char** argv,
int* exec_argc,
const char*** exec_argv) {
//... 注册内部模块。 此处暂时不细讲。
RegisterBuiltinModules();
//... 处理参数,打印 help 等。
ProcessArgv(argv, exec_argv, false);
//...
}
2.2 接着让我们看看里面这个 Start 方法做了什么。同样位于 src/node.cc 中:
inline int Start(uv_loop_t* event_loop,
const std::vector<std::string>& args,
const std::vector<std::string>& exec_args) {
//... 开始创建 Isolate 实例。
Isolate* const isolate = NewIsolate(allocator.get(), event_loop);
//...
{
//... 又是一个 Start 。
exit_code = Start(isolate, isolate_data.get(), args, exec_args);
}
// isolate 销毁。
isolate->Dispose();
//...
return exit_code;
}
参数检查什么的就略过了,上来先创建了一个 Isolate 实例,这个实例相当于是一个 js 独立环境,更粗略一点,比作一个页面。 中间又调用了一个 Start 方法,最终处理一下 isolate 的销毁。
3. 那接着来看这个 Start 方法(麻木了,都叫 Start 方法。)同样位于 src/node.cc 中:
inline int Start(Isolate* isolate, IsolateData* isolate_data,
const std::vector<std::string>& args,
const std::vector<std::string>& exec_args) {
//... 创建一个 Context
Local<Context> context = NewContext(isolate); // 1.
//... 创建一个 Environment 实例,并开启 Start 方法。
Environment env(isolate_data, context, v8_platform.GetTracingAgentWriter());
env.Start(args, exec_args, v8_is_profiling); // 2.
{
//... 环境加载
LoadEnvironment(&env); // 3.
//...
}
{
//...
do {
// 事件循环启动。libuv 相关。 4.
uv_run(env.event_loop(), UV_RUN_DEFAULT);
//...
} while (more == true);
//...
}
//...
const int exit_code = EmitExit(&env);
//... 善后工作,资源回收等等。
return exit_code;
}
Context 又是 v8 的一个概念,相当于执行上下文,js 的执行上下文,可以实现互不影响。比如一个页面上嵌套了某个页面,那么他们之间的 js 上下文环境就不一样。此处需要关注 1 , 2,3,4 四个方法。
3.1 先来看看 1 ,如何创建的 Context。NewContext 同样位于 src/node.cc 中:
Local<Context> NewContext(Isolate* isolate,
Local<ObjectTemplate> object_template) {
// 使用 v8 的 api 创建 Context。
auto context = Context::New(isolate, nullptr, object_template);
// ...
{
// ... Run lib/internal/per_context.js
// 获取 per_context.js 文件的字符串。
Local<String> per_context = NodePerContextSource(isolate);
// 编译运行,v8的模板代码。
ScriptCompiler::Source per_context_src(per_context, nullptr);
Local<Script> s = ScriptCompiler::Compile(
context,
&per_context_src).ToLocalChecked();
s->Run(context).ToLocalChecked();
}
return context;
}
此方法不仅仅创建了一个 Context,而且还预加载执行了一段js。注意这个 NodePerContextSource 方法只有编译过才会有这个文件。
3.1.1 看一下这个方法.文件位于node_javascript.cc 中:
v8::Local<v8::String> NodePerContextSource(v8::Isolate* isolate) {
return internal_per_context_value.ToStringChecked(isolate);
}
static const uint8_t raw_internal_per_context_value[] = { 39,...}
static struct : public v8::String::ExternalOneByteStringResource {
const char* data() const override {
return reinterpret_cast<const char*>(raw_internal_per_context_value);
}
//...
v8::Local<v8::String> ToStringChecked(v8::Isolate* isolate) {
return v8::String::NewExternalOneByte(isolate, this).ToLocalChecked();
}
} internal_per_context_value;
看到这里应该知道了,就是把 raw_internal_per_context_value 这个数组转成 v8 的字符串返回出去。那么问题来了,这个数组里面到底是什么东西呢。
3.1.2 猜也没法猜,那就打印一下呗。打印数组相关代码如下:
#include <string>
#include <iostream>
static const unsigned char raw_internal_per_context_value[] = {39,...}
int main() {
std::cout << (char *)raw_internal_bootstrap_loaders_value << std::endl;
}
g++ -o test test.cc & ./test 就可以看到内容了。你会惊奇的发现,这不就是 lib/internal/per_context.js 文件的内容吗?是的,的确是这样,他就是把这段文本直接在编译期间就编成C++字符数组,为了在启动的时候加快启动速度,不至于现场去读文件从而引发文件加载速度的等等一系列问题。至于此 js 文件内容,在此先不做讲解。接着让我回到 4~5步的方法2当中。
**3.2 ** env.Start 方法位于 src/env.cc 中:
void Environment::Start(const std::vector<std::string>& args,
const std::vector<std::string>& exec_args,
bool start_profiler_idle_notifier) {
//... 一大堆的 uv 操作等等。
// 设置了 process。
auto process_template = FunctionTemplate::New(isolate());
process_template->SetClassName(FIXED_ONE_BYTE_STRING(isolate(), "process"));
// ...
}
可以看到其中设置了 process 是什么,此处设置了之后,在js里面就可以直接拿到 process 变量了。
3.3 LoadEnvironment 方法在 src/node.cc 中:
void LoadEnvironment(Environment* env) {
//...
// 加载 lib/internal/bootstrap/loaders.js 和 node.js 进来。
// FIXED_ONE_BYTE_STRING 就是一个转换字符串的宏。
Local<String> loaders_name =
FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/loaders.js");
// LoadersBootstrapperSource 是获取 loaders.js 的文件内容。 GetBootstrapper 方法是用来
// 执行 js 的。
MaybeLocal<Function> loaders_bootstrapper =
GetBootstrapper(env, LoadersBootstrapperSource(env), loaders_name);
//...
// 获取 global 对象
Local<Object> global = env->context()->Global();
//...
// 暴露 global 出去,在 js 中可以访问。
global->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "global"), global);
// 创建bind,linked_binding,internal_binding
Local<Function> get_binding_fn =
env->NewFunctionTemplate(GetBinding)->GetFunction(env->context())
.ToLocalChecked();
//...
// 执行 internal/loaders.js,node.js 里面的方法。
if (!ExecuteBootstrapper(env, loaders_bootstrapper.ToLocalChecked(),
arraysize(loaders_bootstrapper_args),
loaders_bootstrapper_args,
&bootstrapped_loaders)) {
return;
}
//...
}
static void GetBinding(const FunctionCallbackInfo<Value>& args) {
// ... 通过参数获取模块名。
Local<String> module = args[0].As<String>();
//... 获取内部模块。此处就是通过2.1步骤中的 RegisterBuiltinModules 宏处理之后的东西来获取的。
node_module* mod = get_builtin_module(*module_v);
Local<Object> exports;
if (mod != nullptr) {
// 调用模块初始化方法。
exports = InitModule(env, mod, module);
}
// ... 设置返回值。
args.GetReturnValue().Set(exports);
}
代码很长,但是条理还是挺清晰的。这里进行了一些绑定操作和一些初始化方法的调用逻辑。此处也可以知道,GetBinding 类似的东西是什么。调用的 js 如何执行需要和 js 一起看才能明白。此处先不讲解了。
3.4 uv_run 这个方法此处也不细讲了。 libuv 这个库还没有详细了解。等待了解之后,补上 libuv 的相关调用分析,此处我们知道,在这里开始执行事件循环了。
结语
讲了这么多,大家应该对 nodejs 的启动流程有了一个大致的了解了吧。虽然开头说少点源码,可是后来还是夹杂了很多的源码,哈哈,有一种上当的感觉。后面再讲讲模块加载,libuv加载的相关东西。这次分析就到此结束吧,大家休息~