夺命23问,3千字带你快速搞清Node模块机制
最近在细学Node
我将从中所学,吸收整理成此笔记,以供日后查阅
这是本系列第2
篇,关于Node
模块机制
下方是Node
模块相关的23
个问题,可以先自己想想,文章下方会给你参考答案
- Node模块机制遵循的是什么规范?
- CommonJS定义模块分为几个部分?
- Node模块一般分为哪两类?有什么不一样?
- 尽管规范中定义的exports、require和module使用起来非常简单,但你知道Node在实现他们的过程中经历了哪些步骤吗?
- 为什么Node核心模块的加载速度比文件模块要快?
- Node会对引入过的模块进行缓存,就像浏览器缓存静态脚本文件类型那样,但有什么不一样?
- 核心模块和文件模块的二次加载有什么异同?
- Node模块的标识符有哪几类?针对不同的标识符,Node是怎么处理的?
- Node模块在文件定位时需要注意哪些点
- 对于不同文件扩展名,其载入方法有什么不同
- exports、require和module从何而来?
- 为什么有exports还要有module.exports?
- 对于.node的模块文件是否需要编译?为什么?
- 你知道Node核心模块放在哪里吗?
- 你了解Node中JS核心模块的编译过程吗?
- 什么是Node中的内建模块吗
- 内建模块的组织形式
- 内建模块的优势是什么?
- 当文件模块依赖核心模块中的内建模块时,如何调用好?
- 内建模块如何将内部东西,提供给JavaScript核心模块调用?
- 核心模块的引入流程
- 如何编写模块?
- 聊聊模块之间的调用关系是怎样的?
1. Node模块机制遵循的是什么规范?
Node遵循的是CommonJS
规范
2. CommonJS定义模块分为几个部分?
CommonJS定义模块可分为以下3
部分
- 模块引用 使用require()引入模块
- 模块定义 使用module和exports定义或者说导出模块,其中exports是module上的属性
- 模块标识 其实就是传给require()的参数,它肯可能是是小驼峰的字符串,也可能是以.或..开头的相对路径,当然也可能是绝对路径,文件后缀.js可省
3. Node模块一般分为哪两类?有什么不一样?
Node中的模块分为两类:
- Node自带的模块,也称为核心模块
- 用户自己编写的模块,也称为文件模块
区别除了说核心模块开箱既有之外,还有一个重要区别是核心模块的加载速度更快
4. 尽管规范中定义的exports、require和module使用起来非常简单,但你知道Node在实现他们的过程中经历了哪些步骤吗?
Node引用模块时需要经历3个步骤:
- 路径分析
- 文件定位
- 编译执行
5. 为什么Node核心模块的加载速度比文件模块要快?
核心模块在Node源代码的编译过程中,就已经编译进了二进制执行文件,在Node进程启动时,部分核心模块就被直接加载进内存中,所以在引入这些模块时,文件定位和编译执行就可省略,并且在路径分析中优先判断
而文件模块加载时,需要完整的路径分析+文件定位+编译执行。所以对比下来就知道,核心模块的加载速度比文件模块要快得多
6. Node会对引入过的模块进行缓存,就像浏览器缓存静态脚本文件类型那样,但有什么不一样?
他们都会缓存以减少二次开销,不同的是,浏览器仅仅是缓存文件,Node缓存的却是编译和执行后的对象
7. 核心模块和文件模块的二次加载有什么异同?
相同点是,无论是核心模块还是文件模块,相同模块的二次加载都是优先从缓存加载,这是第一优先级
不同点在于,核心模块的缓存检查优先于文件模块的缓存检查
8. Node模块的标识符有哪几类?针对不同的标识符,Node是怎么处理的?
Node模块的标识符说的其实就是传递给require()的东西,细分一下,主要有以下几类:
- 核心模块,如http、fs、path等
- 相对路径文件模块,以.或者..开头的
- 绝对路径文件模块,以/开头
- 非路径模块形式的文件模块,比如自定义模块
针对不同的标识符,Node会区别对待
核心模块在Node源代码编译过程中已经编译成了二进制代码,加载过程最快;如果尝试加载一个与核心模块标识符相同的自定义模块,是不会被加载成功的
对于路径形式的文件模块,包括相对和绝对路径的文件模块,Node在分析模块时,require()方法会将路径转成真实路径,并以真实路径作为索引,将编译执行后的结果存放到缓存中,方便二次加载。由于文件模块给Node指明了文件的位置,所以在查找过程节约了很多时间,当然加载速度慢与核心模块
对于自定义模块,它是一种特殊的文件模块,可能是一个文件或者包,所以这类模块的查找就最耗时了,特别是路径很深时,它会逐级查找
9. Node模块在文件定位时需要注意哪些点
在文件定位过程中,需要注意文件扩展名分析和目录及包的处理
- 文件扩展名分析 如果require()传递的标识是不包含文件的扩展名,那么Node会按.js、.json、.node的次序不全扩展名,而后依次尝试定位文件。在尝试过程中,需要调用fs同步阻塞式的判断文件是否存在,所以这里有两个小技巧:一是,如果是.node和.json文件,传递给require()时带上扩展名,速度会快一点,另外一点是,同步配合缓存,可以大幅度缓解Node单线程中堵塞式调用的缺陷
- 目录分析和包 在分析标识符的过程中,可能没有找到对应的文件,但却得到一个目录,此时Node会将目录当做一个包来处理
那么,Node首先在当前目录查找package.json,通过JSON.parse()解析出包的描述对象,从中取出main属性指定的文件名进行定位,如果文件缺少扩展名,将会进入到扩展名分析的步骤。而如果main属性指定的文件名错误或压根没有package.json文件,Node就会把index当做默认文件名,然后依次查找index.js、index.json、index.node
如果在目录分析过程中没有成功定位,则自定义模块进入下一个模块路径进行查找。如果模块路径数组都遍历完依然没找到目标文件,则抛出查找失败的异常
10. 对于不同文件扩展名,其载入方法有什么不同
- .js文件 通过fs模块同步读取文件后编译执行
- .node文件 这是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件
- .json 文件 通过fs模块同步读取文件后,用JSON.parse()解析返回结果
- 其余扩展名文件 他们都会被当成.js文件载入
11. exports、require和module从何而来?
其实不只是exports、require和module,还包括常见的_filename和_dirname
node会在编译js模块文件的时,对内容进行头尾包装,通过以下的形式
(function(exports,require,module,_filename,_dirname){xxx}
exports、require和module就来源于这里
12. 为什么有exports还要有module.exports?
exports对象是通过形参的形式传入的,当直接赋值形参时会改变形参原本的引用,但并不改变作用域外的值
13. 对于.node的模块文件是否需要编译?为什么?
不需要
因为它是编写C/C++模块之后编译生成的,所以只有加载和执行过程
14. 你知道Node核心模块放在哪里吗?
其实这得分两部分来说,Node核心模块分为C/C++编写的和JavaScript编写的
对于C/C++编写的文件,存放在Node项目下的src目录下
对于JavaScript编写的文件,则放置在lib目录下
15. 你了解Node中JS核心模块的编译过程吗?
JS核心模块的编译过程需要知道两点吧:
- 将JS核心模块转存为C/C++代码 Node采用V8附带的js2c.py工具,将内置的JS代码转换为C++中的数组,包括src/node.js和lib/*.js文件,生成node_natives.h头文件
需要注意在这个过程中,js代码是以字符串的形式存储在node命名空间中,并且不可以直接执行。在启动node进程时,js代码才被加载到内存中。在加载模块过程中,js核心模块经历标识符分析后直接定位到内存中
- 编译JS核心模块 由于在lib下的所有核心模块都没有require、module、exports这些东西,在引入JS核心模块的过程中,会经历头尾包装的过程,然后才执行和导出exports对象
16. 什么是Node中的内建模块吗
在Node的核心模块中,有些模块时通过C/C++编写的,有些是通过js编写的,还有一些则是有C/C++完成核心部分,剩余部分由JS实现包装和向外导出。通常由纯C/C++编写的部分就被统称为内建模块,内建模块通常不被用户直接调用
17. 内建模块的组织形式
- 定义形式 在Node的内建模块都是由C/C++编写的的模块,这些模块的内部结构定义是
struct node_module_struct{...}
- 将模块定义到node命名空间 通过NODE_MODULE宏,将模块定义到node命名空间
- 统一放置 node_extensions.h文件会将这些散列的内置模块统一放置,放入一个叫node_module_list的数组中
- 取模块 Node通过get_builtin_module()方法就可以很方便的取出node_module_list中的模块
18. 内建模块的优势是什么?
一个字:快
主要体现在一下两点
- 核心模块本身是C/C++编写,性能上天然优于脚本语言
- 在进行文件编译时,它们会编译进二进制文件,一旦Node运行,它们就被直接加载进内存了,无需警告标识定位、文件定位和编译过程,即可直接执行
19. 当文件模块依赖核心模块中的内建模块时,如何调用好?
Node的所有模块可能存在一种依赖层级关系,比如文件模块依赖核心模块,核心模块依赖内建模块,当文件模块依赖底层的内建模块时,一般是不推荐直接调用的,可以通过调用核心模块来实现
原因是核心模块中基本都封装了内建模块
那内建模块如何将内部东西,提供给JavaScript核心模块调用呢,继续看
20. 内建模块如何将内部东西,提供给JavaScript核心模块调用?
Node在启动时,会生成一个全局变量process,并提供Binding方法来协助加载内建模块
21. 核心模块的引入流程
我们知道Node采用的CommonJs模块规范,用户层面使用require()引入模块的方式,非常简洁,但是其内部的引入流程是相当的复杂,它要经历:
- C/C++层面的内建模块定义
- js核心模块的定义和引入
- 文件模块层面的引入
22. 如何编写模块?
其实这需要分两种情况:
- 内置模块(纯C/C++模块);由于作为用户,我们应该是用不到的,这里就不介绍(关于这边部分,如果想了解可以参看相关资料)
- 另外一种是文件模块以及核心模块中的JS部分的模块;这类模块仅需遵循CommonJS规范即可,并且上下文拥有require、module、exports以及可以使用Node定义的全局变量,可以很方便的定义一个模块
23. 聊聊模块之间的调用关系是怎样的?
在聊这个问题之前,我们首先要梳理一下有哪些
Node模块可细分为以下几个:
- 文件模块 我们自己写的js模块
- JS核心模块 系统自带的模块
- C/C++内建模块 一般特指C/C++编写的自带模块 C/C++内建模块属于最底层模块,也属于最核心的模块,主要提供API给JS核心模块和第三方的文件模块使用,但不推荐给第三方模块直接使用
JS核心模块分两种情况:
- 一是,作为C/C++内建模块的封装层和桥接层,最后也给第三方文件模块使用
- 二是,纯粹的功能模块,不需要跟调用底层模块
最后是文件模块,也是平时写的最多的,这包括JS模块和C/C++扩展模块,主要是给其他的普通模块调用
参考
《深入浅出node.js
》
THE END
以上就是本文的所有内容,如有问题欢迎留言🌹~
转载自:https://juejin.cn/post/7042535749014519816