該文章當前使用的nodejs版本是v13.1.0(網上那些分析nodejs源碼的文章不寫清基於的版本都是耍流氓),很是乾貨的一篇文章,請耐心閱讀,不然建議收藏javascript
閱讀本篇文章以前請先閱讀前置文章:html
讀完本篇文章你會掌握:java
首先,nodejs提供那麼多模塊,以及能在各個平臺上跑的飛起,不是由於js很牛逼,而是由於底層依賴了一些你不知道的技術。最大的兩個依賴即是v8和libuv。爲何這麼說呢?由於一個幫助你將js代碼轉變成能夠在各個平臺和機器上運行的機器碼,另一個幫助你調用平臺和機器上各類系統特性,包括操做文件、監聽socket等等。先撇開這兩個最大的依賴,咱們看一下nodejs源碼中的deps
目錄都有些啥?node
上圖即是Nodejs依賴的包,在官網咱們能夠找到裏面一些依賴包的介紹:Dependenciespython
resolve()
族函數。其餘幾個沒在官網提到的這裏也說一下:webpack
node debug
命令。由於是要面向Javascript開發人員,因此咱們不可能直接上來就寫C++/C代碼,那麼確定須要一個東西去封裝這些C++/C代碼,並提供一套優雅的接口給開發者,因而Nodejs就是幹這事的。一言以蔽之:git
Nodejs封裝了全部與底層交流的信息,給開發者提供一致的接口定義。在不斷升級v8和libuv的同時,依然可以作到接口的一致性,這個就是nodejs想要實現的目標。
複製代碼
那麼問題來了,nodejs究竟是怎麼將libuv和v8封裝起來並提供接口的?搞懂這一切以前,咱們先看看Nodejs的目錄結構,這個目錄結構在後面的講解中有用到:github
nodejs源碼有兩個重要的目錄:web
lib
: 包含了全部nodejs函數和模塊的javascript實現,這些實現都是能夠直接在你js項目中引用進去的面試
src
: 包含了全部函數的C++版本實現,這裏的代碼纔會真正引用Libuv和V8。
而後咱們隨便查看一個lib目錄下的文件能夠看到,除了正常的js語法以外,出現了一個在平時應用程序沒有見到的方法:internalBinding
。這個是啥?有啥做用?
咱們的探索之旅即是從這個方法開始,一步步深刻到nodejs內部,一步步帶你們揭開nodejs的神祕面紗。首先咱們要從nodejs的編譯過程提及。
再講編譯過程以前,咱們還得普及一下Nodejs源碼內部的模塊分類和C++加載綁定器兩個概念。
nodejs模塊能夠分爲下面三類:
http
,fs
等等好比lib
目錄下的fs.js
就是native模塊,而fs.js
調用的src
目錄下的node_fs.cc
就是內建模塊。知道了模塊的分類,那麼好奇這些模塊是怎麼加載進來的呢?(本文非講解模塊加載的,因此第三方模塊不在討論範圍內)
後面會有文字涉及到這幾個概念:
NODE_BUILTIN_MODULE_CONTEXT_AWARE()
來建立,而且它們的nm_flags都設置爲NM_F_BUILTIN
NODE_MODULE_CONTEXT_AWARE_CPP()
宏來建立,其flag設置爲NM_F_LINKED
NODE_MODULE_CONTEXT_AWARE_INTERNAL()
宏來建立,其flag設置爲NM_F_INTERNAL
根據官網的推薦,源碼編譯簡單粗暴:
$ ./configure
$ make -j4
複製代碼
咱們能夠從nodejs編譯配置文件中提取出一些重要信息。
衆所周知,Nodejs使用了GYP的編譯方式,其GYP編譯文件是:node.gyp
,咱們從該文件的兩處地方獲取到兩個重要的信息。
從該文件的target
字段能夠看到,編譯以後會生成多個target,可是最重要的是第一個target
,其配置:
{
// 定義的'node_core_target_name%'就是'node',
'target_name': '<(node_core_target_name)',
'type': 'executable', // 這裏的類型是可執行文件
'defines': [
'NODE_WANT_INTERNALS=1',
],
'includes': [
'node.gypi'
],
'include_dirs': [
'src',
'deps/v8/include'
],
'sources': [
'src/node_main.cc'
],
... ...
}
複製代碼
由此可知,整個node應用程序的入口文件其實就是node_main.cc
。
編譯文件的第二個target是libnode
,它是將其他剩餘的C++文件編譯成庫文件,可是有一個特殊的地方就是該target在編譯以前有個action:
{
// 這裏定義的'node_lib_target_name'就是libnode
'target_name': '<(node_lib_target_name)',
'type': '<(node_intermediate_lib_type)',
'includes': [
'node.gypi',
],
'include_dirs': [
'src',
'<(SHARED_INTERMEDIATE_DIR)' # for node_natives.h
],
... ...
'actions': [
{
'action_name': 'node_js2c',
'process_outputs_as_sources': 1,
'inputs': [
# Put the code first so it's a dependency and can be used for invocation.
'tools/js2c.py',
'<@(library_files)',
'config.gypi',
'tools/js2c_macros/check_macros.py'
],
'outputs': [
'<(SHARED_INTERMEDIATE_DIR)/node_javascript.cc',
],
'conditions': [
[ 'node_use_dtrace=="false" and node_use_etw=="false"', {
'inputs': [ 'tools/js2c_macros/notrace_macros.py' ]
}],
[ 'node_debug_lib=="false"', {
'inputs': [ 'tools/js2c_macros/nodcheck_macros.py' ]
}],
[ 'node_debug_lib=="true"', {
'inputs': [ 'tools/js2c_macros/dcheck_macros.py' ]
}]
],
'action': [
'python', '<@(_inputs)',
'--target', '<@(_outputs)',
],
},
],
複製代碼
從這個配置信息來看是說有個js2c.py
的python文件會將lib/**/*.js
和deps/**/*.js
的全部js文件按照其ASCII碼轉化爲一個個數組放到node_javascript.cc
文件中。
生成的node_javascript.cc
文件內容大體以下:
namespace node {
namespace native_module {
...
static const uint8_t fs_raw[] = {...}
...
void NativeModuleLoader::LoadJavaScriptSource() {
...
source_.emplace("fs", UnionBytes{fs_raw, 50659});
...
}
UnionBytes NativeModuleLoader::GetConfig() {
return UnionBytes(config_raw, 3017); // config.gypi
}
}
複製代碼
這種作法直接就將js文件全都緩存到內存,避免了多餘的I/O操做,提升了效率。
所以從上述配置信息咱們能夠總結出這樣一張編譯過程:
好了,清楚了編譯流程以後,咱們再從nodejs的啓動過程來分析internalBinding
究竟是何方神聖。
上一小節咱們知道nodejs應用程序的入口文件是node_main.cc
,因而咱們從這個文件開始追蹤代碼,獲得如下一個流程圖:
其中標註紅色的是須要關注的重點,裏面有些知識和以前的那些文章能夠聯繫起來,若是你閱讀過耗時兩個月,網上最全的原創nodejs深刻系列文章(長達十來萬字的文章,歡迎收藏)中列舉的一些基礎文章,看到這裏,相信有種恍然大悟的感受,感受知識點一會兒均可以聯繫起來了,這就是系統學習的魅力~
回到上圖,全部的線索都聚焦到了這個函數中:NativeModuleLoader::LookupAndCompile
。在調用這個函數以前,還有一個重點就是:此時NativeModuleLoader是實例化的,因此其構造函數是被執行掉的,而其構造函數執行的只有一個函數:LoadJavaScriptSource(),該函數就是上一小節咱們看到node_javascript.cc文件中的函數,因而咱們有如下結論:
internal/bootstrap/loader.js
是咱們執行的第一個js文件那麼NativeModuleLoader::LookupAndCompile
都作了些什麼呢?
NativeModuleLoader::LookupAndCompile
它利用咱們傳入的文件id(此次傳遞的是internal/bootstrap/loader.js
)在_source
變量中查找,找到以後將整個文件內容包裹起來成爲一個新的函數,並追加進一些函數的定義(此次傳遞的是getLinkedBinding
和getInternalBinding
)以便在js文件中能夠調用這些C++函數,而後執行該新函數。這個參數的傳遞是在上圖中的Environment::BootstrapInternalLoaders
函數中:
MaybeLocal<Value> Environment::BootstrapInternalLoaders() {
EscapableHandleScope scope(isolate_);
// Create binding loaders
std::vector<Local<String>> loaders_params = {
process_string(),
FIXED_ONE_BYTE_STRING(isolate_, "getLinkedBinding"),
FIXED_ONE_BYTE_STRING(isolate_, "getInternalBinding"),
primordials_string()};
// 這裏的GetInternalBinding即是咱們調用`getInternalBinding`執行的函數。若是你不知道爲何js能夠調用C++函數的話,請參考這篇文章:《如何正確地使用v8嵌入到咱們的C++應用中》
std::vector<Local<Value>> loaders_args = {
process_object(),
NewFunctionTemplate(binding::GetLinkedBinding)
->GetFunction(context())
.ToLocalChecked(),
NewFunctionTemplate(binding::GetInternalBinding)
->GetFunction(context())
.ToLocalChecked(),
primordials()};
...
}
複製代碼
這個時候加載進loader.js
以後,咱們來看看該文件作了些啥?
internal/bootstrap/loader.js
這個文件很是特殊,是惟一一個沒有出現require關鍵詞的js文件,它惟一使用的外部函數就是剛纔提到的getLinkedBinding和getInternalBinding,這一點能夠經過文件源碼進行覈實
該文件就是構建出NativeModule
這麼一個對象,裏面有一些原型方法,最後返回這麼一個數據結構:
const loaderExports = {
internalBinding,
NativeModule,
require: nativeModuleRequire
};
複製代碼
在裏面咱們找到了internalBinding
這個方法的原始實現:
let internalBinding;
{
const bindingObj = Object.create(null);
// eslint-disable-next-line no-global-assign
internalBinding = function internalBinding(module) {
let mod = bindingObj[module];
if (typeof mod !== 'object') {
// 這裏調用咱們的C++方法
mod = bindingObj[module] = getInternalBinding(module);
moduleLoadList.push(`Internal Binding ${module}`);
}
return mod;
};
}
複製代碼
接着咱們順藤摸瓜,看上圖的流程圖的一個紅色線,loader.js
執行完後的返回值繼續傳遞到了internal/bootstrap/node.js
這個文件使用。
代碼以下:
MaybeLocal<Value> Environment::BootstrapInternalLoaders() {
... ...
// 這裏的loader_exports即是執行完loader.js以後返回的值
Local<Value> loader_exports;
if (!ExecuteBootstrapper(
this, "internal/bootstrap/loaders", &loaders_params, &loaders_args)
.ToLocal(&loader_exports)) {
return MaybeLocal<Value>();
}
CHECK(loader_exports->IsObject());
Local<Object> loader_exports_obj = loader_exports.As<Object>();
// 此時internal_binding_loader的值即是loader_exports.internalBinding,下面的同理
Local<Value> internal_binding_loader =
loader_exports_obj->Get(context(), internal_binding_string())
.ToLocalChecked();
CHECK(internal_binding_loader->IsFunction());
set_internal_binding_loader(internal_binding_loader.As<Function>());
// 注意這裏的require是native_module的require,有別於第三方包的reuqire
Local<Value> require =
loader_exports_obj->Get(context(), require_string()).ToLocalChecked();
CHECK(require->IsFunction());
set_native_module_require(require.As<Function>());
...
}
MaybeLocal<Value> Environment::BootstrapNode() {
... ...
std::vector<Local<Value>> node_args = {
process_object(),
native_module_require(),
internal_binding_loader(), // 這個就是剛纔的那個internalBinding
Boolean::New(isolate_, is_main_thread()),
Boolean::New(isolate_, owns_process_state()),
primordials()};
... ...
}
複製代碼
該文件同理,也會注入isMainThread
、ownsProcessState
以及process
、require
、primordials
和internalBinding
六個C++函數供js文件調用。
由此又獲得的一個結論就是:
可是到這裏,咱們的問題還有一些沒有解開,還須要繼續深刻。
GetInternalBinding
在internal/bootstrap/node.js
中,大部分都是給process
和global
對象賦值初始化,按照上面給的結論,當咱們調用internalBinding
的時候,實際會執行的是GetInternalBinding
這個C++函數。因此咱們來看看這個函數的實現。
js調用C++函數的規則在如何正確地使用v8嵌入到咱們的C++應用中文章中已經說起過,因此咱們就再也不贅述這個是怎麼調用的,咱們關注重點:
void GetInternalBinding(const FunctionCallbackInfo<Value>& args) {
... ...
// 查找模塊,在哪裏查找?
node_module* mod = FindModule(modlist_internal, *module_v, NM_F_INTERNAL);
if (mod != nullptr) {
exports = InitModule(env, mod, module);
// 什麼是constants模塊?
} else if (!strcmp(*module_v, "constants")) {
exports = Object::New(env->isolate());
CHECK(
exports->SetPrototype(env->context(), Null(env->isolate())).FromJust());
DefineConstants(env->isolate(), exports);
} else if (!strcmp(*module_v, "natives")) {
exports = native_module::NativeModuleEnv::GetSourceObject(env->context());
// Legacy feature: process.binding('natives').config contains stringified
// config.gypi
CHECK(exports
->Set(env->context(),
env->config_string(),
native_module::NativeModuleEnv::GetConfigString(
env->isolate()))
.FromJust());
} else {
return ThrowIfNoSuchModule(env, *module_v);
}
// 這裏導出了exports這個變量~
args.GetReturnValue().Set(exports);
}
複製代碼
這個函數又留給了咱們一些疑問:
constants
和natives
的呢?爲了揭開這些問題,咱們繼續往下深刻。
NODE_MODULE_CONTEXT_AWARE_INTERNAL
這個時候NODE_MODULE_CONTEXT_AWARE_INTERNAL
隆重登場,細心的童鞋確定發現諸如src/node_fs.cc
這種文件都是以這個宏定義結束的。
在node_binding.h
文件中能夠找到其定義:
#define NODE_MODULE_CONTEXT_AWARE_INTERNAL(modname, regfunc) \
NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, nullptr, NM_F_INTERNAL
複製代碼
能夠看到實際調用的是宏定義NODE_MODULE_CONTEXT_AWARE_CPP
,只是將flag設置爲NM_F_INTERNAL
。
而NODE_MODULE_CONTEXT_AWARE_CPP
宏定義則實際上調用了方法:node_module_register
。
node_module_register
這個方法即是往全局的靜態變量modlist_internal
和modlist_linked
兩個鏈表掛載模塊:
if (mp->nm_flags & NM_F_INTERNAL) {
mp->nm_link = modlist_internal;
modlist_internal = mp;
} else if (!node_is_initialized) {
// "Linked" modules are included as part of the node project.
// Like builtins they are registered *before* node::Init runs.
mp->nm_flags = NM_F_LINKED;
mp->nm_link = modlist_linked;
modlist_linked = mp;
} else {
thread_local_modpending = mp;
}
複製代碼
因而modlist_internal就是一個鏈表,裏面連接着全部內建模塊,因此上面的GetInternalBinding
方法是這樣的一個執行邏輯:
上圖中的那些internalBinding
的調用,提供了各類各樣的模塊名,其中就有咱們剛纔問到constants
和natives
這兩個特殊的模塊名。
這樣,上面的兩個問題就迎刃而解了。
可是,問題真的全解決完了嗎?若是僅僅是單純地編譯文件的話,這個NODE_MODULE_CONTEXT_AWARE_INTERNAL
是不會被調用的,那麼哪裏來的調用node_module_register
?
🙆,就欣賞大家這種執着的精神。最後的這個問題,連同整篇文章的一個總結性的流程一塊兒釋放給你們,算是個大彩蛋~
上圖即是一個完整的nodejs和libuv以及v8一塊兒合做的流程圖,其中有一個點解釋了剛纔的問題:何時把全部內建模塊都加載到modlist_internal
的?答案就是nodejs啓動的時候調用binding::RegisterBuiltinModules()
。
至此,按理說整篇文章是能夠結束了的,但爲了鞏固咱們以前的學(zhuang)習(bi),咱們仍是決定以一個例子來看看以前在如何正確地使用v8嵌入到咱們的C++應用中文章中講的那麼多理論,是否是在Nodejs源碼中都是對的?
假設有這麼一個index.js:
const fs = require('fs')
module.exports = () => {
fs.open('test.js', () => {
// balabala
})
}
複製代碼
當你在命令行敲入node index.js
回車以後,會有哪些處理流程?
這道題真的太TMD像「當你在瀏覽器輸入某個url回車以後,會通過哪些流程」了。還好,這不是面試(頗有可能會成爲面試題哦~)
你們一看也就是兩三行代碼嗎?可是就這麼簡單的兩三行代碼,能夠出不少面試題哦~好比說:
require
能夠不用聲明而直接引用?還有好多題目能夠問,這裏就不一一列舉了,想要更多問題歡迎留言(😏)
今天咱們重點不在這些面試題,而是驗證C++代碼是否是如以前文章寫的那樣。咱們一行一行解析過去(不會太深刻)。
require('fs')
當你require
的時候,實際上nodejs不直接執行您在js文件中編寫的任何代碼(除了上面提到的internal/bootstrap/loader.js
和internal/bootstrap/node.js
)。它將您的代碼放入一個包裝器函數中,而後執行該包裝函數。這就是將在任何模塊中定義的頂級變量保留在該模塊範圍內的緣由。
好比:
~ $ node
> require('module').wrapper
[ '(function (exports, require, module, __filename, __dirname) { ',
'\n});' ]
>
複製代碼
能夠看到該包裝器函數有5個參數:exports
, require
, module
, __filename
和__dirname
. 因此你在js文件中寫的那些require、module.exports其實都是這些形參,而不是真的全局變量
更多細節就不展開了,要不真的就說不完了~
fs.open
open的js文件就不關注了,最終是調用了:
binding.open(pathModule.toNamespacedPath(path),
flagsNumber,
mode,
req);
複製代碼
接着咱們跳到node_fs.cc
中,一步步校驗以前的理論。
Initialize
還記得上圖中那個終極彩蛋裏,當調用internalBinding
的時候,是會初始化對應的內建模塊,也就是調用其初始化函數,這裏即是Initialize
函數。
這個函數一開始即是給target
設置method,好比:
env->SetMethod(target, "close", Close);
env->SetMethod(target, "open", Open);
複製代碼
那麼該方法最後都是調用了that->Set(context, name_string, function).Check();
,這個是否是和咱們在如何正確地使用v8嵌入到咱們的C++應用中中的第二小節二、調用 C++ 函數講的如出一轍?
接着開始暴露FSReqCallback
這個類,這個在fs.js
文件中有調用到:
const req = new FSReqCallback();
req.oncomplete = callback;
複製代碼
那麼這個時候咱們就要用到如何正確地使用v8嵌入到咱們的C++應用中中的第三小節三、使用 C++ 類的知識了:
Local<FunctionTemplate> fst = env->NewFunctionTemplate(NewFSReqCallback);
fst->InstanceTemplate()->SetInternalFieldCount(1);
fst->Inherit(AsyncWrap::GetConstructorTemplate(env));
Local<String> wrapString =
FIXED_ONE_BYTE_STRING(isolate, "FSReqCallback");
fst->SetClassName(wrapString);
target
->Set(context, wrapString,
fst->GetFunction(env->context()).ToLocalChecked())
.Check();
複製代碼
完美契合了以前講的那些理論知識。
接着咱們看看是如何使用libuv的
Open
異步調用統一封裝了一個叫作AsyncCall
的函數,它又調用了AsyncDestCall
:
AsyncCall(env, req_wrap_async, args, "open", UTF8, AfterInteger,
uv_fs_open, *path, flags, mode);
複製代碼
以後的調用依舊按照咱們以前在fs.c提供的示例同樣,只是爲了封裝,將不少東西隱藏起來,閱讀起來比較費勁。
到這裏,💐你完成了本篇文章的閱讀,也感謝你的耐心讓你又掌握了一塊知識,還沒讀懂的話,點個收藏,之後遇到的時候能夠拿出來參考參考~
感恩~