Node.js的模块分为用户JS模块、Node.js原生JS模块、Node.js内置C++模块。本章介绍这些模块加载的原理以及Node.js中模块加载器的类型和原理。
下面我们以一个例子为开始,分析Node.js中模块加载的原理。假设我们有一个文件demo.js,代码如下

  1. const myjs= require(‘myjs);
  2. const net = require(‘net’);

其中myjs的代码如下

  1. exports.hello = world’;

我们看一下执行node demo.js的时候,过程是怎样的。在Node.js启动章节我们分析过,Node.js启动的时候,会执行以下代码。
require(‘internal/modules/cjs/loader’).Module.runMain(process.argv[1])
其中runMain函数在pre_execution.js的initializeCJSLoader中挂载

  1. function initializeCJSLoader() {
  2. const CJSLoader = require('internal/modules/cjs/loader');
  3. CJSLoader.Module._initPaths();
  4. CJSLoader.Module.runMain =
  5. require('internal/modules/run_main').executeUserEntryPoint;
  6. }

我们看到runMain是run_main.js导出的函数。继续往下看

  1. const CJSLoader = require('internal/modules/cjs/loader');
  2. const { Module } = CJSLoader;
  3. function executeUserEntryPoint(main = process.argv[1]) {
  4. const resolvedMain = resolveMainPath(main);
  5. const useESMLoader = shouldUseESMLoader(resolvedMain);
  6. if (useESMLoader) {
  7. runMainESM(resolvedMain || main);
  8. } else {
  9. Module._load(main, null, true);
  10. }
  11. }
  12. module.exports = {
  13. executeUserEntryPoint
  14. };

process.argv[1]就是我们要执行的JS文件。最后通过cjs/loader.js的Module._load加载了我们的JS。下面我们看一下具体的处理逻辑。

  1. Module._load = function(request, parent, isMain) {
  2. const filename = Module._resolveFilename(request, parent, isMain);
  3. const cachedModule = Module._cache[filename];
  4. // 有缓存则直接返回
  5. if (cachedModule !== undefined) {
  6. updateChildren(parent, cachedModule, true);
  7. if (!cachedModule.loaded)
  8. return getExportsForCircularRequire(cachedModule);
  9. return cachedModule.exports;
  10. }
  11. // 是否是可访问的原生JS模块,是则返回
  12. const mod = loadNativeModule(filename, request);
  13. if (mod && mod.canBeRequiredByUsers) return mod.exports;
  14. // 非原生JS模块,则新建一个Module表示加载的模块
  15. const module = new Module(filename, parent);
  16. // 缓存
  17. Module._cache[filename] = module;
  18. // 加载
  19. module.load(filename);
  20. // 调用方拿到的是module.exports的值
  21. return module.exports;
  22. };

_load函数主要是三个逻辑
1 判断是否有缓存,有则返回。
2 没有缓存,则判断是否是原生JS模块,是则交给原生模块处理。
1 不是原生模块,则新建一个Module表示用户的JS模块,然后执行load函数加载。
这里我们只需要关注3的逻辑,在Node.js中,用户定义的模块使用Module表示。

  1. function Module(id = '', parent) {
  2. // 模块对应的文件路径
  3. this.id = id;
  4. this.path = path.dirname(id);
  5. // 在模块里使用的exports变量
  6. this.exports = {};
  7. this.parent = parent;
  8. // 加入父模块的children队列
  9. updateChildren(parent, this, false);
  10. this.filename = null;
  11. // 是否已经加载
  12. this.loaded = false;
  13. this.children = [];
  14. }

接着看一下load函数的逻辑。

  1. Module.prototype.load = function(filename) {
  2. this.filename = filename;
  3. // 拓展名
  4. const extension = findLongestRegisteredExtension(filename);
  5. // 根据拓展名使用不同的加载方式
  6. Module._extensions[extension](this, filename);
  7. this.loaded = true;
  8. };

Node.js会根据不同的文件拓展名使用不同的函数处理。

19.1 加载用户模块

在Node.js中_extensions有三种,分别是js、json、node。

19.1.1 加载JSON模块

加载JSON模块是比较简单的

  1. Module._extensions['.json'] = function(module, filename) {
  2. const content = fs.readFileSync(filename, 'utf8');
  3. try {
  4. module.exports = JSONParse(stripBOM(content));
  5. } catch (err) {
  6. err.message = filename + ': ' + err.message;
  7. throw err;
  8. }
  9. };

直接读取JSON文件的内容,然后解析成对象就行。

19.1.2 加载JS模块

  1. Module._extensions['.js'] = function(module, filename) {
  2. const content = fs.readFileSync(filename, 'utf8');
  3. module._compile(content, filename);
  4. };

读完文件的内容,然后执行_compile

  1. Module.prototype._compile = function(content, filename) {
  2. // 生成一个函数
  3. const compiledWrapper = wrapSafe(filename, content, this);
  4. const dirname = path.dirname(filename);
  5. // require是对_load函数的封装
  6. const require = (path) => {
  7. return this.require(path);
  8. };
  9. let result;
  10. // 我们平时使用的exports变量
  11. const exports = this.exports;
  12. const thisValue = exports;
  13. // 我们平时使用的module变量
  14. const module = this;
  15. // 执行函数
  16. result = compiledWrapper.call(thisValue,
  17. exports,
  18. require,
  19. module,
  20. filename,
  21. dirname);
  22. return result;
  23. }

_compile里面包括了几个重要的逻辑
1 wrapSafe:包裹我们的代码并生成一个函数
2 require:支持在模块内加载其他模块
3 执行模块代码
我们看一下这三个逻辑。
1 wrapSafe

  1. function wrapSafe(filename, content, cjsModuleInstance) {
  2. const wrapper = Module.wrap(content);
  3. return vm.runInThisContext(wrapper, {
  4. filename,
  5. lineOffset: 0,
  6. ...
  7. });
  8. }
  9. const wrapper = [
  10. '(function (exports, require, module, __filename, __dirname) { ',
  11. '\n});'
  12. ];
  13. Module.wrap = function(script) {
  14. return Module.wrapper[0] + script + Module.wrapper[1];
  15. };

vm.runInThisContext的第一个参数是”(function() {})”的时候,会返回一个函数。所以执行Module.wrap后会返回一个字符串,内容如下

  1. (function (exports, require, module, __filename, __dirname) {
  2. //
  3. });

接着我们看一下require函数,即我们平时在代码中使用的require。
2 require

  1. Module.prototype.require = function(id) {
  2. requireDepth++;
  3. try {
  4. return Module._load(id, this, /* isMain */ false);
  5. } finally {
  6. requireDepth--;
  7. }
  8. };

require是对Module._load的封装,Module._load会把模块导出的变量通过module.exports属性返回给require调用方。因为Module._load只会从原生JS模块和用户JS模块中查找用户需要加载的模块,所以是无法访问C模块的,访问C模块可用process.bindng或internalBinding。
3 执行代码
我们回到_compile函数。看一下执行vm.runInThisContext返回的函数。

  1. compiledWrapper.call(exports,
  2. exports,
  3. require,
  4. module,
  5. filename,
  6. dirname);

相当于执行以下代码

  1. (function (exports, require, module, __filename, __dirname) {
  2. const myjs= require(‘myjs);
  3. const net = require(‘net’);
  4. });

至此,Node.js开始执行用户的JS代码。刚才我们我们已经分析过require是对Module._load的封装,当执行require加载用户模块时,又回到了我们正在分析的这个过程。

19.1.3 加载node模块

Node拓展的模块本质上是动态链接库,我们看require一个.node模块的时候的过程。我们从加载.node模块的源码开始。

  1. Module._extensions['.node'] = function(module, filename) {
  2. // ...
  3. return process.dlopen(module, path.toNamespacedPath(filename));
  4. };

直接调了process.dlopen,该函数在node.js里定义。

  1. const rawMethods = internalBinding('process_methods');
  2. process.dlopen = rawMethods.dlopen;

找到process_methods模块对应的是node_process_methods.cc。

  1. env->SetMethod(target, "dlopen", binding::DLOpen);

之前说过,Node.js的拓展模块其实是动态链接库,那么我们先看看一个动态链接库我们是如何使用的。以下是示例代码。

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <dlfcn.h>
  4. int main(){
  5. // 打开一个动态链接库,拿到一个handler
  6. handler = dlopen('xxx.so',RTLD_LAZY);
  7. // 取出动态链接库里的函数add
  8. add = dlsym(handler,"add");
  9. // 执行
  10. printf("%d",add(1,1));
  11. dlclose(handler);
  12. return 0;
  13. }

了解动态链接库的使用,我们继续分析刚才看到的DLOpen函数。

  1. void DLOpen(const FunctionCallbackInfo<Value>& args) {
  2. int32_t flags = DLib::kDefaultFlags;
  3. node::Utf8Value filename(env->isolate(), args[1]); // Cast
  4. env->TryLoadAddon(*filename, flags, [&](DLib* dlib) {
  5. const bool is_opened = dlib->Open();
  6. node_module* mp = thread_local_modpending;
  7. thread_local_modpending = nullptr;
  8. // 省略部分代码
  9. if (mp->nm_context_register_func != nullptr) {
  10. mp->nm_context_register_func(exports,
  11. module,
  12. context,
  13. mp->nm_priv);
  14. } else if (mp->nm_register_func != nullptr) {
  15. mp->nm_register_func(exports, module, mp->nm_priv);
  16. }
  17. return true;
  18. });
  19. }

我们看到重点是TryLoadAddon函数,该函数的逻辑就是执行它的第三个参数。我们发现第三个参数是一个函数,入参是DLib对象。所以我们先看看这个类。

  1. class DLib {
  2. public:
  3. static const int kDefaultFlags = RTLD_LAZY;
  4. DLib(const char* filename, int flags);
  5. bool Open();
  6. void Close();
  7. const std::string filename_;
  8. const int flags_;
  9. std::string errmsg_;
  10. void* handle_;
  11. uv_lib_t lib_;
  12. };

再看一下实现。

  1. bool DLib::Open() {
  2. handle_ = dlopen(filename_.c_str(), flags_);
  3. if (handle_ != nullptr) return true;
  4. errmsg_ = dlerror();
  5. return false;
  6. }

DLib就是对动态链接库的一个封装,它封装了动态链接库的文件名和操作。TryLoadAddon函数首先根据require传入的文件名,构造一个DLib,然后执行

  1. const bool is_opened = dlib->Open();

Open函数打开了一个动态链接库,这时候我们要先了解一下打开一个动态链接库究竟发生了什么。首先我们一般C++插件最后一句代码的定义。

  1. NAPI_MODULE(NODE_GYP_MODULE_NAME, init)

这是个宏定义。

  1. #define NAPI_MODULE(modname, regfunc) \
  2. NAPI_MODULE_X(modname, regfunc, NULL, 0)
  3. #define NAPI_MODULE_X(modname, regfunc, priv, flags) \
  4. static napi_module _module = \
  5. { \
  6. NAPI_MODULE_VERSION, \
  7. flags, \
  8. __FILE__, \
  9. regfunc, \
  10. #modname, \
  11. priv, \
  12. {0}, \
  13. }; \
  14. static void _register_modname(void) __attribute__((constructor)); \
  15. static void _register_modname(void) { \
  16. napi_module_register(&_module); \
  17. }

所以一个node扩展就是定义了一个napi_module模块和一个register_modname(modname是我们定义的)函数。__attribute((constructor))是代表该函数会先执行的意思,具体可以查阅文档。看到这里我们知道,当我们打开一个动态链接库的时候,会执行_register_modname函数,该函数执行的是

  1. napi_module_register(&_module);

我们继续展开。

  1. // Registers a NAPI module.
  2. void napi_module_register(napi_module* mod) {
  3. node::node_module* nm = new node::node_module {
  4. -1,
  5. mod->nm_flags | NM_F_DELETEME,
  6. nullptr,
  7. mod->nm_filename,
  8. nullptr,
  9. napi_module_register_cb,
  10. mod->nm_modname,
  11. mod, // priv
  12. nullptr,
  13. };
  14. node::node_module_register(nm);
  15. }

Node.js把napi模块转成node_module。最后调用node_module_register。

  1. extern "C" void node_module_register(void* m) {
  2. struct node_module* mp = reinterpret_cast<struct node_module*>(m);
  3. if (mp->nm_flags & NM_F_INTERNAL) {
  4. mp->nm_link = modlist_internal;
  5. modlist_internal = mp;
  6. } else if (!node_is_initialized) {
  7. mp->nm_flags = NM_F_LINKED;
  8. mp->nm_link = modlist_linked;
  9. modlist_linked = mp;
  10. } else {
  11. thread_local_modpending = mp;
  12. }
  13. }

napi模块不是NM_F_INTERNAL模块,node_is_initialized是在Node.js初始化时设置的变量,这时候已经是true。所以注册napi模块时,会执行thread_local_modpending = mp。thread_local_modpending 类似一个全局变量,保存当前加载的模块。分析到这,我们回到DLOpen函数。

  1. node_module* mp = thread_local_modpending;
  2. thread_local_modpending = nullptr;

这时候我们就知道刚才那个变量thread_local_modpending的作用了。node_module* mp = thread_local_modpending后我们拿到了我们刚才定义的napi模块的信息。接着执行node_module的函数nm_register_func。

  1. if (mp->nm_context_register_func != nullptr) {
  2. mp->nm_context_register_func(exports,
  3. module,
  4. context,
  5. mp->nm_priv);
  6. } else if (mp->nm_register_func != nullptr) {
  7. mp->nm_register_func(exports, module, mp->nm_priv);
  8. }

从刚才的node_module定义中我们看到函数是napi_module_register_cb。

  1. static void napi_module_register_cb(v8::Local<v8::Object> exports,
  2. v8::Local<v8::Value> module,
  3. v8::Local<v8::Context> context,
  4. void* priv) {
  5. napi_module_register_by_symbol(exports, module, context,
  6. static_cast<napi_module*>(priv)->nm_register_func);
  7. }

该函数调用napi_module_register_by_symbol函数,并传入napi_module的nm_register_func函数。

  1. void napi_module_register_by_symbol(v8::Local<v8::Object> exports,
  2. v8::Local<v8::Value> module,
  3. v8::Local<v8::Context> context,
  4. napi_addon_register_func init) {
  5. // Create a new napi_env for this specific module.
  6. napi_env env = v8impl::NewEnv(context);
  7. napi_value _exports;
  8. env->CallIntoModuleThrow([&](napi_env env) {
  9. _exports = init(env, v8impl::JsValueFromV8LocalValue(exports));
  10. });
  11. if (_exports != nullptr &&
  12. _exports != v8impl::JsValueFromV8LocalValue(exports)) {
  13. napi_value _module = v8impl::JsValueFromV8LocalValue(module);
  14. napi_set_named_property(env, _module, "exports", _exports);
  15. }
  16. }

init就是我们定义的函数。入参是env和exports,可以对比我们定义的函数的入参。最后我们修改exports变量。即设置导出的内容。最后在JS里,我们就拿到了C++层定义的内容。

19.2 加载原生JS模块

上一节我们了解了Node.js执行node demo.js的过程,其中我们在demo.js中使用require加载net模块。net是原生JS模块。这时候就会进入原生模块的处理逻辑。
原生模块是Node.js内部实现的JS模块。使用NativeModule来表示。

  1. class NativeModule {
  2. // 原生JS模块的map
  3. static map = new Map(moduleIds.map((id) => [id, new NativeModule(id)]));
  4. constructor(id) {
  5. this.filename = `${id}.js`;
  6. this.id = id;
  7. this.canBeRequiredByUsers = !id.startsWith('internal/');
  8. this.exports = {};
  9. this.loaded = false;
  10. this.loading = false;
  11. this.module = undefined;
  12. this.exportKeys = undefined;
  13. }
  14. }

当我们执行require(‘net’)时,就会进入_load函数。_load函数判断要加载的模块是原生JS模块后,会通过loadNativeModule函数加载原生JS模块。我们看这个函数的定义。

  1. function loadNativeModule(filename, request) {
  2. const mod = NativeModule.map.get(filename);
  3. if (mod) {
  4. mod.compileForPublicLoader();
  5. return mod;
  6. }
  7. }

在Node.js启动过程中我们分析过,mod是一个NativeModule对象,接着看compileForPublicLoader。

  1. compileForPublicLoader() {
  2. this.compileForInternalLoader();
  3. return this.exports;
  4. }
  5. compileForInternalLoader() {
  6. if (this.loaded || this.loading) {
  7. return this.exports;
  8. }
  9. // id就是我们要加载的模块,比如net
  10. const id = this.id;
  11. this.loading = true;
  12. try {
  13. const fn = compileFunction(id);
  14. fn(this.exports,
  15. // 加载原生JS模块的加载器
  16. nativeModuleRequire,
  17. this,
  18. process,
  19. // 加载C++模块的加载器
  20. internalBinding,
  21. primordials);
  22. this.loaded = true;
  23. } finally {
  24. this.loading = false;
  25. }
  26. return this.exports;
  27. }

我们重点看compileFunction这里的逻辑。该函数是node_native_module_env.cc模块导出的函数。具体的代码就不贴了,通过层层查找,最后到node_native_module.cc 的NativeModuleLoader::CompileAsModule

  1. MaybeLocal<Function> NativeModuleLoader::CompileAsModule(
  2. Local<Context> context,
  3. const char* id,
  4. NativeModuleLoader::Result* result) {
  5. Isolate* isolate = context->GetIsolate();
  6. // 函数的形参
  7. std::vector<Local<String>> parameters = {
  8. FIXED_ONE_BYTE_STRING(isolate, "exports"),
  9. FIXED_ONE_BYTE_STRING(isolate, "require"),
  10. FIXED_ONE_BYTE_STRING(isolate, "module"),
  11. FIXED_ONE_BYTE_STRING(isolate, "process"),
  12. FIXED_ONE_BYTE_STRING(isolate, "internalBinding"),
  13. FIXED_ONE_BYTE_STRING(isolate, "primordials")};
  14. // 编译出一个函数
  15. return LookupAndCompile(context, id, &parameters, result);
  16. }

我们继续看LookupAndCompile。

  1. MaybeLocal<Function> NativeModuleLoader::LookupAndCompile(
  2. Local<Context> context,
  3. const char* id,
  4. std::vector<Local<String>>* parameters,
  5. NativeModuleLoader::Result* result) {
  6. Isolate* isolate = context->GetIsolate();
  7. EscapableHandleScope scope(isolate);
  8. Local<String> source;
  9. // 找到原生JS模块内容所在的内存地址
  10. if (!LoadBuiltinModuleSource(isolate, id).ToLocal(&source)) {
  11. return {};
  12. }
  13. // ‘net’ + ‘.js’
  14. std::string filename_s = id + std::string(".js");
  15. Local<String> filename =
  16. OneByteString(isolate,
  17. filename_s.c_str(),
  18. filename_s.size());
  19. // 省略一些参数处理
  20. // 脚本源码
  21. ScriptCompiler::Source script_source(source, origin, cached_data);
  22. // 编译出一个函数
  23. MaybeLocal<Function> maybe_fun =
  24. ScriptCompiler::CompileFunctionInContext(context,
  25. &script_source,
  26. parameters->size(),
  27. parameters->data(),
  28. 0,
  29. nullptr,
  30. options);
  31. Local<Function> fun = maybe_fun.ToLocalChecked();
  32. return scope.Escape(fun);
  33. }

LookupAndCompile函数首先找到加载模块的源码,然后编译出一个函数。我们看一下LoadBuiltinModuleSource如何查找模块源码的。

  1. MaybeLocal<String> NativeModuleLoader::LoadBuiltinModuleSource(Isolate* isolate, const char* id) {
  2. const auto source_it = source_.find(id);
  3. return source_it->second.ToStringChecked(isolate);
  4. }

这里是id是net,通过该id从_source中找到对应的数据,那么_source是什么呢?因为Node.js为了提高效率,把原生JS模块的源码字符串直接转成ASCII码存到内存里。这样加载这些模块的时候,就不需要硬盘IO了。直接从内存读取就行。我们看一下_source的定义(在编译Node.js源码或者执行js2c.py生成的node_javascript.cc中)。

  1. source_.emplace("net", UnionBytes{net_raw, 46682});
  2. source_.emplace("cyb", UnionBytes{cyb_raw, 63});
  3. source_.emplace("os", UnionBytes{os_raw, 7548});

cyb是我增加的测试模块。我们可以看一下该模块的内容。

  1. static const uint8_t cyb_raw[] = {
  2. 99,111,110,115,116, 32, 99,121, 98, 32, 61, 32,105,110,116,101,114,110, 97,108, 66,105,110,100,105,110,103, 40, 39, 99,
  3. 121, 98, 95,119,114, 97,112, 39, 41, 59, 32, 10,109,111,100,117,108,101, 46,101,120,112,111,114,116,115, 32, 61, 32, 99,
  4. 121, 98, 59
  5. };

我们转成字符串看一下是什么

  1. Buffer.from([99,111,110,115,116, 32, 99,121, 98, 32, 61, 32,105,110,116,101,114,110, 97,108, 66,105,110,100,105,110,103, 40, 39, 99,
  2. 121, 98, 95,119,114, 97,112, 39, 41, 59, 32, 10,109,111,100,117,108,101, 46,101,120,112,111,114,116,115, 32, 61, 32, 99,
  3. 121, 98, 59].join(',').split(',')).toString('utf-8')

输出

  1. const cyb = internalBinding('cyb_wrap');
  2. module.exports = cyb;

所以我们执行require(‘net’)时,通过NativeModule的compileForInternalLoader,最终会在_source中找到net模块对应的源码字符串,然后编译成一个函数。

  1. const fn = compileFunction(id);
  2. fn(this.exports,
  3. // 加载原生JS模块的加载器
  4. nativeModuleRequire,
  5. this,
  6. process,
  7. // 加载C++模块的加载器
  8. internalBinding,
  9. primordials);

由fn的入参可以知道,我们在net(或其它原生JS模块中)只能加载原生JS模块和内置的C模块。当fn执行完毕后,原生模块加载器就会把mod.exports的值返回给调用方。
19.3 加载内置C模块
在原生JS模块中我们一般会加载一些内置的C++模块,这是Node.js拓展JS功能的关键之处。比如我们require(‘net’)的时候,net模块会加载tcp_wrap模块。

  1. const {
  2. TCP,
  3. TCPConnectWrap,
  4. constants: TCPConstants
  5. } = internalBinding('tcp_wrap')

C++模块加载器也是在internal/bootstrap/loaders.js中定义的,分为三种。
1 internalBinding:不暴露给用户的访问的接口,只能在Node.js代码中访问,比如原生JS模块(flag为NM_F_INTERNAL)。

  1. let internalBinding;
  2. {
  3. const bindingObj = ObjectCreate(null);
  4. internalBinding = function internalBinding(module) {
  5. let mod = bindingObj[module];
  6. if (typeof mod !== 'object') {
  7. mod = bindingObj[module] = getInternalBinding(module);
  8. moduleLoadList.push(`Internal Binding ${module}`);
  9. }
  10. return mod;
  11. };
  12. }

internalBinding是在getInternalBinding函数基础上加了缓存功能。getInternalBinding是C层定义的函数对JS暴露的接口名。它的作用是从C模块链表中找到对应的模块。
2 process.binding:暴露给用户调用C模块的接口,但是只能访问部分C模块(flag为NM_F_BUILTIN的C++模块)。

  1. process.binding = function binding(module) {
  2. module = String(module);
  3. if (internalBindingWhitelist.has(module)) {
  4. return internalBinding(module);
  5. }
  6. throw new Error(`No such module: ${module}`);
  7. };

binding是在internalBinding的基础上加了白名单的逻辑,只对外暴露部分模块。

  1. const internalBindingWhitelist = new SafeSet([
  2. 'async_wrap',
  3. 'buffer',
  4. 'cares_wrap',
  5. 'config',
  6. 'constants',
  7. 'contextify',
  8. 'crypto',
  9. 'fs',
  10. 'fs_event_wrap',
  11. 'http_parser',
  12. 'icu',
  13. 'inspector',
  14. 'js_stream',
  15. 'natives',
  16. 'os',
  17. 'pipe_wrap',
  18. 'process_wrap',
  19. 'signal_wrap',
  20. 'spawn_sync',
  21. 'stream_wrap',
  22. 'tcp_wrap',
  23. 'tls_wrap',
  24. 'tty_wrap',
  25. 'udp_wrap',
  26. 'url',
  27. 'util',
  28. 'uv',
  29. 'v8',
  30. 'zlib'
  31. ]);

3 process._linkedBinding: 暴露给用户访问C模块的接口,用于访问用户自己添加的但是没有加到内置模块的C模块(flag为NM_F_LINKED)。

  1. const bindingObj = ObjectCreate(null);
  2. process._linkedBinding = function _linkedBinding(module) {
  3. module = String(module);
  4. let mod = bindingObj[module];
  5. if (typeof mod !== 'object')
  6. mod = bindingObj[module] = getLinkedBinding(module);
  7. return mod;
  8. };

_linkedBinding是在getLinkedBinding函数基础上加了缓存功能,getLinkedBinding是C层定义的函数对外暴露的名字。getLinkedBinding从另一个C模块链表中查找对应的模块。
上一节已经分析过,internalBinding是加载原生JS模块时传入的实参。internalBinding是对getInternalBinding的封装。getInternalBinding对应的是binding::GetInternalBinding(node_binding.cc)。

  1. // 根据模块名查找对应的模块
  2. void GetInternalBinding(const FunctionCallbackInfo<Value>& args) {
  3. Environment* env = Environment::GetCurrent(args);
  4. // 模块名
  5. Local<String> module = args[0].As<String>();
  6. node::Utf8Value module_v(env->isolate(), module);
  7. Local<Object> exports;
  8. // 从C++内部模块找
  9. node_module* mod = FindModule(modlist_internal,
  10. *module_v,
  11. NM_F_INTERNAL);
  12. // 找到则初始化
  13. if (mod != nullptr) {
  14. exports = InitModule(env, mod, module);
  15. } else {
  16. // 省略
  17. }
  18. args.GetReturnValue().Set(exports);
  19. }

modlist_internal是一条链表,在Node.js启动过程的时候,由各个C模块连成的链表。通过模块名找到对应的C模块后,执行InitModule初始化模块。

  1. // 初始化一个模块,即执行它里面的注册函数
  2. static Local<Object> InitModule(Environment* env,
  3. node_module* mod,
  4. Local<String> module) {
  5. Local<Object> exports = Object::New(env->isolate());
  6. Local<Value> unused = Undefined(env->isolate());
  7. mod->nm_context_register_func(exports, unused, env->context(), mod->nm_priv);
  8. return exports;
  9. }

执行C模块的nm_context_register_func指向的函数。这个函数就是在C模块最后一行定义的Initialize函数。Initialize会设置导出的对象。我们从JS可以访问Initialize导出的对象。V8中,JS调用C++函数的规则是函数入参const FunctionCallbackInfo& args(拿到JS传过来的内容)和设置返回值args.GetReturnValue().Set(给JS返回的内容), GetInternalBinding函数的逻辑就是执行对应模块的钩子函数,并传一个exports变量进去,然后钩子函数会修改exports的值,该exports的值就是JS层能拿到的值。