夺命23问,3千字带你快速搞清Node模块机制

·  阅读 2328
夺命23问,3千字带你快速搞清Node模块机制

最近在细学Node

我将从中所学,吸收整理成此笔记,以供日后查阅

这是本系列第2篇,关于Node模块机制

下方是Node模块相关的23个问题,可以先自己想想,文章下方会给你参考答案

  1. Node模块机制遵循的是什么规范?
  2. CommonJS定义模块分为几个部分?
  3. Node模块一般分为哪两类?有什么不一样?
  4. 尽管规范中定义的exports、require和module使用起来非常简单,但你知道Node在实现他们的过程中经历了哪些步骤吗?
  5. 为什么Node核心模块的加载速度比文件模块要快?
  6. Node会对引入过的模块进行缓存,就像浏览器缓存静态脚本文件类型那样,但有什么不一样?
  7. 核心模块和文件模块的二次加载有什么异同?
  8. Node模块的标识符有哪几类?针对不同的标识符,Node是怎么处理的?
  9. Node模块在文件定位时需要注意哪些点
  10. 对于不同文件扩展名,其载入方法有什么不同
  11. exports、require和module从何而来?
  12. 为什么有exports还要有module.exports?
  13. 对于.node的模块文件是否需要编译?为什么?
  14. 你知道Node核心模块放在哪里吗?
  15. 你了解Node中JS核心模块的编译过程吗?
  16. 什么是Node中的内建模块吗
  17. 内建模块的组织形式
  18. 内建模块的优势是什么?
  19. 当文件模块依赖核心模块中的内建模块时,如何调用好?
  20. 内建模块如何将内部东西,提供给JavaScript核心模块调用?
  21. 核心模块的引入流程
  22. 如何编写模块?
  23. 聊聊模块之间的调用关系是怎样的?

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

以上就是本文的所有内容,如有问题欢迎留言🌹~

分类:
前端
分类:
前端
收藏成功!
已添加到「」, 点击更改