内容简介:之前用过一段时间的Helloworld 镇楼:写过 nodejs 的都能看懂如上代码。寥寥数行,就创建了一个 http 服务。第一行代码,就出现了一个 require 关键字,那么 require 是从何而来呢?带着这个问题,我们一起去看下吧。
之前用过一段时间的 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 testtest.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.3LoadEnvironment 方法在 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.4uv_run 这个方法此处也不细讲了。 libuv 这个库还没有详细了解。等待了解之后,补上 libuv 的相关调用分析,此处我们知道,在这里开始执行事件循环了。
结语
讲了这么多,大家应该对 nodejs 的启动流程有了一个大致的了解了吧。虽然开头说少点源码,可是后来还是夹杂了很多的源码,哈哈,有一种上当的感觉。后面再讲讲模块加载,libuv加载的相关东西。这次分析就到此结束吧,大家休息~
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 【Tomcat学习笔记】启动流程分析--总体流程
- 【Tomcat学习笔记】启动流程分析--总体流程
- Launcher 启动流程
- Kubelet 启动流程分析
- Kubelet 启动流程分析
- Launcher 启动流程
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Code Reading
Diomidis Spinellis / Addison-Wesley Professional / 2003-06-06 / USD 64.99
This book is a unique and essential reference that focuses upon the reading and comprehension of existing software code. While code reading is an important task faced by the vast majority of students,......一起来看看 《Code Reading》 这本书的介绍吧!