杂谈: math = require(‘math’)

当我们在文件中输入const math = require('math')时候,究竟发生了什么?

如果我们在当前文件平级目录node_modules中有math.js文件模块,那么我们可以通过const math = require('math')的方式引入math模块,然后在当前文件中使用定义好的方法。

也可以通过const math = require('./math.js')的方式,在指定的文件math.js中引入math模块,然后在当前文件中使用定义好的方法。

一个引入的是math,一个引入的是./math.js,那么它两之间有什么区别,以及require到底是什么,本文就一步步寻找答案…

首先,这两种本行代码是属于CommonJs范畴的语法,先看CommonJs规范。

一、CommonJs

1、模块定义

在模块中,存在一个module对象,表示当前模块本身,在node中,一个文件就是一个模块。

module中有exports属性,是模块导出的唯一出口,其上可以挂载属性或者方法。

exports.add = function (a, b) {
    return a + b;
}

exports.constantValue = 1;

2、模块引用

CommonJs规范中,模块的引入通过require(xxx)来完成,为当前执行上下文引入模块xxx

模块的引入方式又分为手动写入路径和系统自动查找路径的方式:

(1)手动写入
// math.js文件
exports.add = function (a, b) {
    return a + b;
}
// index.js文件
const math = require('./math')
console.log(math.add(1, 2))

可以看出,我们定义了文件math.js,通过exports.add的方式为模块定义了方法add,再通过const math = require('./math')的方式将模块进行导入,路径是手动写入的路径"./math"

(2)系统查找
// node_modules/math.js文件
exports.add = function (a, b) {
    return a + b;
}
exports.moduleInfo = module;
// index.js文件
const math = require('math')
console.log(math.moduleInfo);

如果,在同级文件下有文件夹node_modules,在其中定义了math.js文,那么就可以通过const math = require('math')的方式直接引入。这是什么原因?

接着看…

我们通过exports.moduleInfo = module的方式将当前模块信息暴露出去,在index.js文件中通过console.log(math.moduleInfo)的方式进行打印。执行结果如下:

image.png

可以发现,模块查询的路径是从与当前执行文件最近的位置开始,然后一层一层向上,直到在根文件夹下的node_modules中也没找到才终止,显然在当前例子中,在paths数组的第一个元素路径下,我们就找到了math.js,寻找结束。

(3)模块标识

模块标识指的是在引入模块过程require(xxx)中的xxx,它根据模块引入的方式可以分为:

  • 手动写入:绝对路径或者相对路径,指的是特定文件
  • 系统查找:符合小驼峰的字符串,指的是模块名称,会被按照系统引入的方式去寻找。

CommonJs中,一个模块的引入会经历模块定义模块标识模块标识这三个过程。通过CommonJs的学习,我们可以知道math = require('math')math = require('./math.js')两种引入方式的区别是模块标识的区别,一个是手动引入,一个是系统查找

那么,在node环境中,该规范中的require是如何实现的呢?我们接着看…

二、Node中模块

1、Node中模块引入的流程

(1)路径分析
  • 核心模块:模块已经编译为了二进制代码,所以路径就是二进制代码的真实路径。
  • 文件模块:在路径分析的时候,require直接将其转换成真实的文件路径。
  • 自定义模块:自定义文件会返回一个路径数组,是从当前文件开始的node_modules,直到根目录下的node_modules。如:
    'E:\\learn\\webpack\\module_learn\\node_modules',
    'E:\\learn\\webpack\\node_modules',
    'E:\\learn\\node_modules',
    'E:\\node_modules'
(2)文件定位

    ① 文件

在文件加载的过程中如果不包含扩展名,node会按照.js ==> .json ==> .node的方式进行扩展名补全。

扩展后再通过调用fs模块同步阻塞的进行文件是否存在的分析。

    ② 文件夹

在文件加载的过程中如果没找到文件,但是找到文件夹。

首先查找package.json文件,通过JSON.parse的解析出包对象,从中取出main属性指定的文件名进行定位。

如果未找到package.json文件或者package.json文件解析错误,Node会将index.js文件作为默认文件名,然后依次查找index.jsindex.jsonindex.node

(3)编译执行

先在文件index.js文件我们打印以下代码:

console.log(require) // 第一次打印
const math = require('./math')
console.log(math.add(1, 2));
console.log(require) // 第二次打印

第一个console.log(require)的执行结果为:

image.png

通过执行结果可以分析出:

require支持的文件类型.js.json.node

第一次执行结果中cache可以看出就是当前我们执行的index.js文件。

第二次执行结果的cache部分的区别为:

image.png

可以看出,当执行完const math = require('./math')之后,加载过的文件已经在require函数的cache缓存中。

2、Node中模块分类

  • 核心模块:node自身提供的,在node源码编译的过程中生成二进制执行文件。在node启动过程中,核心模块就被加载到内存中了。所以,可以省略文件定位和编译执行,执行速度也是最快的。
  • 文件模块:用户编写的,在运行时动态引入,需要路径分析、文件定位和编译执行的完成过程,相比核心模块会更慢。
  • 自定义模块:既不是核心模块也不是文件模块。可能是一个文件或者包的形式存在,此类模块查找是最慢的。

从这里可以看出其加载速度为:缓存 > 核心模块 > 文件模块 > 自定义模块

3、加载原则

文件模块和核心模块的加载优先原则都是优先从缓存加载,而且node缓存的是编译和执行之后的对象。

4、require方法的来源

可以在index.js文件中执行:

console.log(require)

执行结果为:

image.png

可以看出其中包含了属性resolvemainextensionscache属性。

那么,为啥可以在文件中直接使用require呢?

其实,在编译过程中,Node对获取的JavaScript文件进行了头部包装。在头部添加了(function (exports, require, module, __filename, __dirname) {\n,在尾部添加了\n}),执行console.log(require)就相当于执行如下逻辑:

(function (exports, require, module, __filename, __dirname) {
    // 可以访问exports, require, module, __filename和__dirname的具体逻辑。
    console.log(require)
});

这里,exports, require, module, __filename__dirname就是模块的参数。

  • exports:就是整个文件中的module.exports,可以通过exports.xxx的方式为其添加属性和方法。
  • require:是可执行方法,可以引入指定路径的文件模块,也可以通过模块标识符进行模块的引入。其中通过extensions表明了require可以引入的文件类型,cache表示其缓存的文件,执行require(xxx)的时候,先从缓存中寻找。
  • module:当前模块。
  • __filename:当前执行文件名称。
  • __dirname:当前执行文件夹名称。

至此,就解释了requirenode中的实现。

总结

当我们在文件中输入const math = require('math')时候,相当于在当前文件模块中执行了模块函数中传入的参数requirerequire包含了其支持的模块引入类型,模块寻找路径和已被缓存的文件模块。

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容