“模块机制” 有十三问——读《深入浅出Node》第二章有感

本文详细解释了Node.js中CommonJS模块的导入机制,包括require()函数的使用、模块定义与导出、路径分析过程、核心模块与文件模块的区别,以及模块缓存和二次加载优化。
摘要由CSDN通过智能技术生成

var math = require(‘math’);

在CommonJS规范中,存在require()方法,这个方法接受模块标识,以此引入一个模块的API到当前上下文中。

2.模块定义

在模块中,上下文提供require()方法来引入外部模块。对应引入的功能,上下文提供了exports对象用于导出当前模块的方法或者变量,并且它是唯一导出的出口。在模块中,还存在一个module对象,它代表模块自身,而exports是module的属性。在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式:

// math.js

exports.add = function () {

var sum = 0,

i = 0,

args = arguments,

l = args.length;

while (i < l) {

sum += args[i++];

}

return sum;

};

在另一个文件中,我们通过require()方法引入模块后,就能调用定义的属性或方法了:

// program.js

var math = require(‘math’);

exports.increment = function (val) {

return math.add(val, 1);

};

模块的定义十分简单,接口也十分简洁。它的意义在于将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出功能以顺畅地连接上下游依赖。如图所示,每个模块具有独立的空间,它们互不干扰,在引用时也显得干净利落。

CommonJS构建的这套模块导出和引入机制使得用户完全不必考虑变量污染,命名空间等方案与之相比相形见绌。

3.模块标识

模块标识其实就是传递给require()方法的参数,它必须是符合小驼峰命名的字符串,或者以..开头的相对路径,或者绝对路径。它可以没有文件名后缀.js。

第三问:上面提到了模块引用,你可以谈谈模块引用的过程吗?


在Node中引入模块,需要经历如下3个步骤。

  • (1) 路径分析

  • (2) 文件定位

  • (3) 编译执行

第四问: Node中所有模块的引用都要经历这些?


非也!

在Node中,模块分为两类:一类是Node提供的模块,称为核心模块;另一类是用户编写的模块,称为文件模块。

  • ❑ 核心模块部分在Node源代码的编译过程中,编译进了二进制执行文件。在Node进程启动时,部分核心模块就被直接加载进内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略掉,并且在路径分析中优先判断,所以它的加载速度是最快的。

  • ❑ 文件模块则是在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢。

第五问: 我觉得还不够全面,特别重要的一点就是模块二次引用的时候,你没讲。


确实,模块二次引用跟第一次是不一样的。

与前端浏览器会缓存静态脚本文件以提高性能一样,Node对引入过的模块都会进行缓存,以减少二次引入时的开销。不同的地方在于,浏览器仅仅缓存文件,而Node缓存的是与前端浏览器会缓存静态脚本文件以提高性能一样,Node对引入过的模块都会进行缓存,以减少二次引入时的开销。不同的地方在于,浏览器仅仅缓存文件,而Node缓存的是编译和执行之后的对象。

不论是核心模块还是文件模块,require()方法对相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级的。不同之处在于核心模块的缓存检查先于文件模块的缓存检查。。

不论是核心模块还是文件模块,require()方法对相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级的。不同之处在于核心模块的缓存检查先于文件模块的缓存检查。

第六问:你能谈谈 模块引用中的路径分析吗?


可以,路径分析其实就是 模块标志符分析

模块标识符在Node中主要分为以下几类。

  • ❑ 核心模块,如http、fs、path等。

  • ❑ .或..开始的相对路径文件模块。

  • ❑ 以/开始的绝对路径文件模块。

  • ❑ 非路径形式的文件模块,如自定义的connect模块。

而这几种标志符的分析都是不同的。

●核心模块

核心模块的优先级仅次于缓存加载,它在Node的源代码编译过程中已经编译为二进制代码,其加载过程最快。

如果试图加载一个与核心模块标识符相同的自定义模块,那是不会成功的。如果自己编写了一个http用户模块,想要加载成功,必须选择一个不同的标识符或者换用路径的方式。

●路径形式的文件模块

以.、..和/开始的标识符,这里都被当做文件模块来处理。在分析文件模块时,require()方法会将路径转为真实路径,并以真实路径作为索引,将编译执行后的结果存放到缓存中,以使二次加载时更快。

由于文件模块给Node指明了确切的文件位置,所以在查找过程中可以节约大量时间,其加载速度慢于核心模块。

●自定义模块

自定义模块指的是非核心模块,也不是路径形式的标识符。它是一种特殊的文件模块,可能是一个文件或者包的形式(通常我们npm install 的包就是属于自定义模块,它是被放在node_modules包里的)。这类模块的查找是最费时的,也是所有方式中最慢的一种。

第七问: 为什么说自定义模块的查找是最慢的?


模块路径是Node在定位文件模块的具体文件时制定的查找策略,具体表现为一个路径组成的数组。关于这个路径的生成规则,我们可以手动尝试一番。

  • (1) 创建module_path.js文件,其内容为console.log(module.paths);。

  • (2) 将其放到任意一个目录中然后执行node module_path.js。

在Linux下,你可能得到的是这样一个数组输出:

[ ‘/home/jackson/research/node_modules’,

‘/home/jackson/node_modules’,

‘/home/node_modules’,

‘/node_modules’ ]

而在Windows下,也许是这样:

[ ‘c:\nodejs\node_modules’, ‘c:\node_modules’ ]

可以看出,模块路径的生成规则如下所示。

  • ❑ 当前文件目录下的node_modules目录。

  • ❑ 父目录下的node_modules目录。

  • ❑ 父目录的父目录下的node_modules目录。

  • ❑ 沿路径向上逐级递归,直到根目录下的node_modules目录。

它的生成方式与JavaScript的原型链或作用域链的查找方式十分类似。在加载的过程中,Node会逐个尝试模块路径中的路径,直到找到目标文件为止。可以看出,当前文件的路径越深,模块查找耗时会越多,这是自定义模块的加载速度是最慢的原因。

第八问: 假如我使用require(“myfile”)引用文件模块,那这个模块分析过程是怎样的。


我觉得需要分两种情况讨论,一种是 当查到的myfile是文件时就需要按照文件扩展名分析,一种是查不到是文件,而是目录或者包时,就需要继续按照 目录分析

●文件扩展名分析

require()在分析标识符的过程中,会出现标识符中不包含文件扩展名的情况。CommonJS模块规范也允许在标识符中不包含文件扩展名,这种情况下,Node会按.js、.json、.node的次序补足扩展名,依次尝试

在尝试的过程中,需要调用fs模块同步阻塞式地判断文件是否存在。因为Node是单线程的,所以这里是一个会引起性能问题的地方。小诀窍是:如果是.node和.json文件,在传递给require()的标识符中带上扩展名,会加快一点速度。另一个诀窍是:同步配合缓存,可以大幅度缓解Node单线程中阻塞式调用的缺陷

●目录分析和包

在分析标识符的过程中,require()通过分析文件扩展名之后,可能没有查找到对应文件,但却得到一个目录,这在引入自定义模块和逐个模块路径进行查找时经常会出现,此时Node会将目录当做一个包来处理。

在这个过程中,Node对CommonJS包规范进行了一定程度的支持。首先,Node在当前目录下查找package.json(CommonJS包规范定义的包描述文件),通过JSON.parse()解析出包描述对象,从中取出main属性指定的文件名进行定位。如果文件名缺少扩展名,将会进入扩展名分析的步骤。

而如果main属性指定的文件名错误,或者压根没有package.json文件,Node会将index当做默认文件名,然后依次查找index.js、index.json、index.node。

如果在目录分析的过程中没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找。如果模块路径数组都被遍历完毕,依然没有查找到目标文件,则会抛出查找失败的异常。

第九问:上面提到模块的引入的最后步骤模块编译了,其实文件定位之后是加载文件,然后编译,你能谈谈不同文件是怎么加载的吗


在Node中,每个文件模块都是一个Module对象,,它的定义如下:

function Module(id, parent) {

this.id = id;

this.exports = {};

this.parent = parent;

if (parent && parent.children) {

parent.children.push(this);

}

this.filename = null;

this.loaded = false;

this.children = [];

}

编译和执行是引入文件模块的最后一个阶段。定位到具体的文件后,Node会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,其载入方法也有所不同,具体如下所示。

  • .js文件。通过fs模块同步读取文件后编译执行。

  • .node文件。这是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件。

  • .json文件。通过fs模块同步读取文件后,用JSON.parse()解析返回结果。

  • ❑ 其余扩展名文件。它们都被当做.js文件载入

每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache对象上,以提高二次引入的性能。

根据不同的文件扩展名,Node会调用不同的读取方式,如.json文件的调用如下:

// Native extension for .json

Module._extensions[‘.json’] = function(module, filename) {

var content = NativeModule.require(‘fs’).readFileSync(filename, ‘utf8’);

try {

module.exports = JSON.parse(stripBOM(content));

} catch (err) {

err.message = filename + ': ’ + err.message;

throw err;

}

};

其中,Module._extensions会被赋值给require()的extensions属性,所以通过在代码中访问require.extensions可以知道系统中已有的扩展加载方式。编写如下代码测试一下:

console.log(require.extensions);

得到的执行结果如下:

{ ‘.js’: [Function], ‘.json’: [Function], ‘.node’: [Function] }

如果想对自定义的扩展名进行特殊的加载,可以通过类似require.extensions['.ext']的方式实现。

在确定文件的扩展名之后,Node将调用具体的编译方式来将文件执行后返回给调用者。

第十问:前面谈到分别有.js ,.node, .json的文件模块。我比较感兴趣的是.js,即JavaScript模块的编译,你能谈谈吗?


自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

更多面试题

**《350页前端校招面试题精编解析大全》**内容大纲主要包括 HTML,CSS,前端基础,前端核心,前端进阶,移动端开发,计算机基础,算法与数据结构,项目,职业发展等等

资料获取方式:点击蓝色传送门免费获取

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

[外链图片转存中…(img-QobQdIJh-1713609017295)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

[外链图片转存中…(img-nJTpydTn-1713609017296)]

更多面试题

**《350页前端校招面试题精编解析大全》**内容大纲主要包括 HTML,CSS,前端基础,前端核心,前端进阶,移动端开发,计算机基础,算法与数据结构,项目,职业发展等等

资料获取方式:点击蓝色传送门免费获取

[外链图片转存中…(img-GIKFrVYZ-1713609017296)]

  • 10
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值