前言
之前用過一段時間的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載入的相關東西。這次分析就到此結束吧,大家休息~