nodejs模块系统源码分析
概述
node.js的出现使得前端工程师可以跨端工作在服务器上,当然,一个新的运行环境的诞生亦会带来新的模块、功能、抑或是思想上的革新,本文将带领读者领略 node.js(以下简称 node) 的模块设计思想以及剖析部分核心源码实现。
commonjs 规范
node 最初遵循 commonjs 规范来实现自己的模块系统,同时做了一部分区别于规范的定制。commonjs 规范是为了解决javascript的作用域问题而定义的模块形式,它可以使每个模块在它自身的命名空间中执行。
该规范强调模块必须通过module.exports导出对外的变量或函数,通过require()来导入其他模块的输出到当前模块作用域中,同时,遵循以下约定:
- 在模块中,必须暴露一个 require 变量,它是一个函数,require 函数接受一个模块标识符,require 返回外部模块的导出的 api。如果要求的模块不能被返回则 require 必须抛出一个错误。
- 在模块中,必须有一个*变量叫做 exports,它是一个对象,模块在执行时可以在 exports 上挂载模块的属性。模块必须使用 exports 对象作为唯一的导出方式。
- 在模块中,必须有一个*变量 module,它也是一个对象。module 对象必须有一个 id 属性,它是这个模块的顶层 id。id 属性必须是这样的,require(module.id)会从源出module.id的那个模块返回 exports 对象(就是说 module.id 可以被传递到另一个模块,而且在要求它时必须返回最初的模块)。
node 对 commonjs 规范的实现
定义了模块内部的 module.require 函数和全局的 require 函数,用来加载模块。
在 node 模块系统中,每个文件都被视为一个独立的模块。模块被加载时,都会初始化为 module 对象的实例,module 对象的基本实现和属性如下所示:
每一个模块都对外暴露自己的 exports 属性作为使用接口。
模块导出以及引用
在 node 中,可使用 module.exports 对象整体导出一个变量或者函数,也可将需要导出的变量或函数挂载到 exports 对象的属性上,代码如下所示:
通过全局 require 函数引用模块,可传入模块名称、相对路径或者绝对路径,当模块文件后缀为 js / json / node 时,可省略后缀,如下代码所示:
注意事项:
exports变量是在模块的文件级作用域内可用的,且在模块执行之前赋值给module.exports。
如果为exports赋予了新值,则它将不再绑定到module.exports,反之亦然:
]当module.exports属性被新对象完全替换时,通常也需要重新赋值exports:
模块系统实现分析模块定位
以下是require函数的代码实现:
上述代码接收给定的模块路径,其中的 requiredepth 用来记载模块加载的深度。其中 module 的类方法_load实现了 node 加载模块的主要逻辑,下面我们来解析module._load函数的源码实现,为了方便大家理解,我把注释加在了文中。
加载策略
上面的代码信息量比较大,我们主要看以下几个问题:
模块的缓存策略是什么? 分析上述代码我们可以看到,_load加载函数针对三种情况给出了不同的加载策略,分别是:
- 情况一:缓存命中,直接返回。
- 情况二:内建模块,返回暴露出来的 exports 属性,也就是 module.exports 的别名。
- 情况三:使用文件或第三方代码生成模块,最后返回,并且缓存,这样下次同样的访问就会去使用缓存而不是重新加载。
module._resolvefilename(request, parent, ismain) 是怎么解析出文件名称的?
我们看如下定义的类方法:
上面的代码中比较突出的是使用了_resolvelookuppaths和_findpath两个方法。
_resolvelookuppaths: 通过接受模块名称和模块调用者,返回提供_findpath使用的遍历范围数组。
_findpath: 依据目标模块和上述函数查找到的范围,找到对应的 filename 并返回。
模块加载
标准模块处理
阅读完上面的代码,我们发现,当遇到模块是一个文件夹的时候会执行trypackage函数的逻辑,下面简要分析一下具体实现。
readpackage 函数负责读取和解析 package.json 文件中的内容,具体描述如下:
上面的两段代码完美地解释 package.json 文件的作用,模块的配置入口( package.json 中的 main 字段)以及模块的默认文件为什么是 index,具体流程如下图所示:
模块文件处理
定位到对应模块之后,该如何加载和解析呢?以下是具体代码分析:
后缀处理
可以看出,针对不同的文件后缀,node.js 的加载方式是不同的,以下针对.js, .json, .node简单进行分析。
.js 后缀 js 文件读取主要通过 node 内置 apifs.readfilesync实现。
json 后缀 json 文件的处理逻辑比较简单,读取文件内容后执行jsonparse即可拿到结果。
.node 后缀 .node 文件是一种由 c / c++ 实现的原生模块,通过 process.dlopen 函数读取,而 process.dlopen 函数实际上调用了 c++ 代码中的 dlopen 函数,而 dlopen 中又调用了 uv_dlopen, 后者加载 .node 文件,类似 os 加载系统类库文件。
从上面的三段源码,我们看出来并且可以理解,只有 js 后缀最后会执行实例方法_compile,我们去除一些实验特性和调试相关的逻辑来简要的分析一下这段代码。
编译执行
模块加载完成后,node 使用 v8 引擎提供的方法构建运行沙箱,并执行函数代码,代码如下所示:
上述代码中,我们可以看到在_compile函数中调用了wrapwrapsafe函数,执行了__dirname / __filename / module / exports / require公共变量的注入,并且调用了 c++ 的 runinthiscontext 方法(位于 src/node_contextify.cc 文件)构建了模块代码运行的沙箱环境,并返回了compiledwrapper对象,最终通过compiledwrapper.call方法运行模块。
以上就是nodejs模块系统源码分析的详细内容,更多关于nodejs模块系统源码分析的资料请关注其它相关文章!
下一篇: NodeJs内存占用过高的排查实战记录