前言
之前用过一段时间的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加载的相关东西。这次分析就到此结束吧,大家休息~
作者:妖怪来了
链接:https://www.jianshu.com/p/c02fec0defcf
共同學習,寫下你的評論
評論加載中...
作者其他優質文章