node 模块载入原理【1】
简单介绍
我们会从简单的模块载入原理来开始,尝试阅读下 Node.js 源代码。首先我们知道 Node.js 的源代码主要是由 C++ 和 JavaScript 编写的,JS 部分主要在 lib 目录下,而 C++ 部分主要在 src 目录下。

模块加载主要是分四种类型的模块:
- C++ 核心模块:主要在 src 目录下,比如 node_file.cc
- Node.js 内部模块:和 C++ 核心模块不同,在源码的 lib 目录下,以同名的 JS 源码来实现,实际上 Node.js 内置模块是对 C++ 核心模块的更高层次的封装,比如 fs、http
- 用户源码模块
- C++ 扩展模块:插件是用 C++ 编写的动态链接共享对象。 require() 函数可以将插件加载为普通的 Node.js 模块。 插件提供了 JavaScript 和 C/C++ 库之间的接口。
这次介绍的是 C++ 核心模块加载的源码分析
C++ 核心模块加载过程
一句话概括:每个 C++ 核心模块源码末尾都有一个宏调用,将模块注册到 C++ 核心模块的链表当中去,以供 internalBinding 获取它。
从这句话可以得到两个步骤:注册和获取。
总的流程

- 入口文件执行
我们知道,Node.js 的执行入口在 node.cc 的 Start 函数
int Start(int argc, char** argv) {}
- 分支一:C++ 模块注册到链表中
我们来看看 Start 函数都做了什么,首先调用了 InitializeOncePerProcess,对 Node.js 和 V8 做初始化处理。从这个函数找下去,可以找到 C++ 模块注册到链表
InitializationResult result = InitializeOncePerProcess(argc, argv);
- 分支二:获取 C++ 模块
在 Start 函数继续找下去,可以看到实例化了 NodeMainInstance,调用了 Run 函数。从这个函数找下去,可以找到获取 C++ 模块的过程
NodeMainInstance main_instance(...);
result.exit_code = main_instance.Run();
分支一:C++ 模块注册到链表中

1、前置链路
从上面的 InitializeOncePerProcess 开始往下找,可以看到调用了初始化 node 的函数 InitializeNodeWithArgs 函数
result.exit_code = InitializeNodeWithArgs(&(result.args), &(result.exec_args), &errors);
继续找,看到它调用了 node_binding.cc 中的 RegisterBuiltinModules,作用是注册 C++ 模块
binding::RegisterBuiltinModules();
2、C++ 模块注册函数的调用
那我们就来看一下,RegisterBuiltinModules 这个函数都做了什么,主要做了两件事:一是定义了 V 这个宏,它接收 modname 这个参数;二是调用 NODE_BUILTIN_MODULES
void RegisterBuiltinModules() {
#define V(modname) _register_##modname();
NODE_BUILTIN_MODULES(V) // module name 可以在这个宏找到,这里不多写了
#undef V
}
然后我们可以看到 NODE_BUILTIN_MODULES 其实也是一个宏定义:
#define NODE_BUILTIN_MODULES(V) \ NODE_BUILTIN_STANDARD_MODULES(V) \
...
其中 NODE_BUILTIN_STANDARD_MODULES 的定义是这样的:
#define NODE_BUILTIN_STANDARD_MODULES(V) \
V(async_wrap) \
V(buffer) \
...
也就是说,c++的预处理后,会变成下面的函数体
void RegisterBuiltinModules() {
_register_async_wrap();
_register_buffer();
.... }
也就是说,最终去调用的是 _register_async_wrap 和 _register_buffer ...这些函数,好了,那么这些函数是在哪定义的呢?
3、C++ 模块注册函数的定义
从上面宏定义后面,可以找到这样的注释:
The definitions are in each module's implementation when calling the NODE_MODULE_CONTEXT_AWARE_INTERNAL.
也就是说,定义是在每个模块调用 NODE_MODULE_CONTEXT_AWARE_INTERNAL 时进行的~
好的于是我们看看 fs 模块对应的 C++ 文件 node_file.cc ,看到最后一行调用了 NODE_MODULE_CONTEXT_AWARE_INTERNAL
NODE_MODULE_CONTEXT_AWARE_INTERNAL(fs, node::fs::Initialize)
继续,在 node_binding.h 看到它调用了 NODE_MODULE_CONTEXT_AWARE_CPP
#define NODE_MODULE_CONTEXT_AWARE_INTERNAL(modname, regfunc) \
NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, nullptr, NM_F_INTERNAL)
继续,在 node_binding.h 中,定义了 _register_##modname 执行时调用了 node_module_register
#define NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, priv, flags) \
void _register_##modname() { node_module_register(&_module); }
好的最后一步,在 node_binding.cc 中找到了它的定义,将传入的 node module 插到了链表 modlist_internal 中,后面查找时就会在这里找了~
extern "C" void node_module_register(void* m) {
struct node_module* mp = reinterpret_cast<struct node_module*>(m);
if (mp->nm_flags & NM_F_INTERNAL) {
mp->nm_link = modlist_internal;
modlist_internal = mp;
}
}
好了~到这里我们就知道,C++ 模块的插入链表的过程了,简单来说就是:每个 C++ 内置模块源码末尾都有一个宏调用,编译模块代码的时候,将模块注册到 C++ 核心模块的链表 modlist_internal 当中去,记住链表的名字哦。
分支二:获取 C++ 模块

1、前置链路
好,这部分从 main_instance.Run() 开始,可以看到他的定义在 node_main_instance.cc 里面
执行了 RunBootstrapping
if (env->RunBootstrapping().IsEmpty()) {
*exit_code = 1;
}
RunBootstrapping 执行了 node.cc 中的 BootstrapInternalLoaders
if (BootstrapInternalLoaders().IsEmpty()) {
return MaybeLocal<Value>();
}
接着它执行了 internal/bootstrap/loaders 这个文件
if (!ExecuteBootstrapper(this, "internal/bootstrap/loaders", &loaders_params, &loaders_args).ToLocal(&loader_exports))
2、从链表获取 C++ 模块
在这个文件的注释中,解释了它的作用:
This file is compiled as if it's wrapped in a function with arguments passed by node::RunBootstrapping()
global process, getLinkedBinding, getInternalBinding, primordials
可以看出这个 js 文件被包裹在一个函数中,这个函数接收四个参数,这四个参数哪里来的呢
再去 C++ 文件里找
// 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()};
std::vector<Local<Value>> loaders_args = {
process_object(),
NewFunctionTemplate(binding::GetLinkedBinding)
NewFunctionTemplate(binding::GetInternalBinding)
primordials()};
嗯,其中 getLinkedBinding 和 getInternalBinding 两个函数,它们会负责 JS 和 C++ 的交互,getInternalBinding 是获取核心模块的函数,另外的一个应该是 C++ 扩展相关的,那我们来看下 GetInternalBinding
在 node_binding.cc 中可以看到,它内部执行了 get_internal_module
node_module* mod = get_internal_module(*module_v);
来~node_binding.cc 里面的 get_internal_module 去执行了 FindModule 传入了刚才我们注册了的链表 modlist_internal,模块标识,和标识内部模块的一个常量
node_module* get_internal_module(const char* name) {
return FindModule(modlist_internal, name, NM_F_INTERNAL);
}
最后我们看看 FindModule
inline struct node_module* FindModule(struct node_module* list,
const char* name,
int flag) {
struct node_module* mp;
for (mp = list; mp != nullptr; mp = mp->nm_link) {
if (strcmp(mp->nm_modname, name) == 0) break;
}
CHECK(mp == nullptr || (mp->nm_flags & flag) != 0);
return mp;
}
嗯,从 modlist_internal 链表中找到模块并返回了,耶
整体的流程图

后续
最后说到 loader.js 接收到 getInternalBinding 就停止了,那在继续看一下,他定义了 process.binding 和 process._linkedBinding 分别对应刚才的 getInternalBinding 和 getLinkedBinding,另外还定义了 internalBinding
internalBinding = function internalBinding(module) {
let mod = bindingObj[module];
if (typeof mod !== 'object') {
mod = bindingObj[module] = getInternalBinding(module);
moduleLoadList.push(`Internal Binding ${module}`);
}
return mod;
};
这个函数用到的地方很多,比如在 fs.js 中,或是通过 CommonJS 或 ESModule 的方式加载 JS 模块时,也会用到这个函数,后面还有很多可以看~
参考文档
- https://juejin.im/post/5d10b607e51d45108f254229
- https://www.zhihu.com/lives/842742839304667136
- https://dev.to/captainsafia/how-does-node-load-built-in-modules-g1k
- https://github.com/tsy77/blog/issues/7
node 模块载入原理【1】的更多相关文章
- 深入了解Node模块原理
深入了解Node模块原理 当我们编写JavaScript代码时,我们可以申明全局变量: var s = 'global'; 在浏览器中,大量使用全局变量可不好.如果你在a.js中使用了全局变量s,那么 ...
- 解密javascript模块载入器require.js
require.config require.config设置require.js模板载入选项 // 定义config req.config = function (config) { return ...
- Commonjs规范及Node模块实现
前面的话 Node在实现中并非完全按照CommonJS规范实现,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性.本文将详细介绍NodeJS的模块实现 引入 nodejs是区别于java ...
- 【转】Commonjs规范及Node模块实现
前言: Node在实现中并非完全按照CommonJS规范实现,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性.本文将详细介绍NodeJS的模块实现 引入 nodejs是区别于javas ...
- [转]模块化——Common规范及Node模块实现
Node在实现中并非完全按照CommonJS规范实现,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性.本文将详细介绍NodeJS的模块实现 引入 nodejs是区别于javascrip ...
- node模块加载层级优化
模块加载痛点 大家也或多或少的了解node模块的加载机制,最为粗浅的表述就是依次从当前目录向上级查询node_modules目录,若发现依赖则加载.但是随着应用规模的加大,目录层级越来越深,若是在某个 ...
- node模块系统常用命令
node模块系统常用命令 命令 示例 备注 安装模块 npm install commander 最新版本 npm install commander@1.0.0 指定版本 npm install c ...
- 模块机制 之commonJs、node模块 、AMD、CMD
在其他高级语言中,都有模块中这个概念,比如java的类文件,PHP有include何require机制,JS一开始就没有模块这个概念,起初,js通过<script>标签引入代码的方式显得杂 ...
- NW.js安装原生node模块node-printer控制打印机
1.安装原生node模块 #全局安装nw-gyp npm install -g nw-gyp #设置目标NW.js版本 set npm_config_target=0.31.4 #设置构建架构,ia3 ...
随机推荐
- 初识Mybatis之工程搭建
简介:MyBatis 是一款优秀的持久层框架,它支持定制化 SQL.存储过程以及高级映射.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以使用简单的 ...
- 从iPhone X到三星S9,为何现在山寨还能如此肆无忌惮?
X到三星S9,为何现在山寨还能如此肆无忌惮?" title="从iPhone X到三星S9,为何现在山寨还能如此肆无忌惮?"> 曾几何时,以"土豪金&qu ...
- Allenmind's Blog
听说,Sass和Compass更配哟.来看看Compass的基本用法! 目录 Compass和Sass 安装Compass 项目初始化 编译 Compass的模块 Compass的Helper函数 一 ...
- C++走向远洋——67(项目二、洗牌)
*/ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:text.cpp * 作者:常轩 * 微信公众号:Worldhe ...
- Windows GDI 窗口与 Direct3D 屏幕截图
前言 Windows 上,屏幕截图一般是调用 win32 api 完成的,如果 C# 想实现截图功能,就需要封装相关 api.在 Windows 上,主要图形接口有 GDI 和 DirectX.GDI ...
- 正式学习MVC 01
1.新建项目 点击创建新项目,选择ASP.NET web应用程序,对项目进行命名后点击创建. 截图如下: 取消勾选HTTPS配置 可选择空 + mvc 或直接选定MVC 2.目录结构分析 1) App ...
- JVM—垃圾回收GC算法
1 GC算法简介 算法 特点 标记-清除 分为"标记"和"清除"两个阶段 复制 可以解决效率问题,将可用的内存按容量划分为大小相等的两块. 标记-整理 先标记. ...
- Flutter 拖拽排序组件 ReorderableListView
注意:无特殊说明,Flutter版本及Dart版本如下: Flutter版本: 1.12.13+hotfix.5 Dart版本: 2.7.0 ReorderableListView是通过长按拖动某一项 ...
- DUBBO 面试灵魂18问
一.Dubbo 是什么 dubbo 是一个分布式框架,是一个远程服务调用的分布式框架,其核心部分包含: 1)集群容错: 提供基于接口方法的透明远程过程调用,包含多协议支持,以及软负债均衡.失败容错.地 ...
- 浅谈Java开发三层架构
三层架构,一般来说就是将整个业务应用划分为:表现层(UI).业务逻辑层(BLL).数据访问层(DAL).区分层次的目的即为了“高内聚,低耦合”的思想. 概念简介 1.表现层(UI):简单来说,就是展现 ...