输入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)
的方式进行打印。执行结果如下:
可以发现,模块查询的路径是从与当前执行文件最近的位置开始,然后一层一层向上,直到在根文件夹下的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.js
、index.json
和index.node
。
(3)编译执行
先在文件index.js文件我们打印以下代码:
console.log(require) // 第一次打印
const math = require('./math')
console.log(math.add(1, 2));
console.log(require) // 第二次打印
第一个console.log(require)
的执行结果为:
通过执行结果可以分析出:
require支持的文件类型
为.js
、.json
和.node
。
第一次执行结果中cache
可以看出就是当前我们执行的index.js
文件。
第二次执行结果的cache
部分的区别为:
可以看出,当执行完const math = require('./math')
之后,加载过的文件已经在require函数的cache缓存中。
2、Node中模块分类
- 核心模块:node自身提供的,在node源码编译的过程中生成二进制执行文件。在node启动过程中,核心模块就被加载到内存中了。所以,可以省略文件定位和编译执行,执行速度也是最快的。
- 文件模块:用户编写的,在运行时动态引入,需要路径分析、文件定位和编译执行的完成过程,相比核心模块会更慢。
- 自定义模块:既不是核心模块也不是文件模块。可能是一个文件或者包的形式存在,此类模块查找是最慢的。
从这里可以看出其加载速度为:缓存 > 核心模块 > 文件模块 > 自定义模块
3、加载原则
文件模块和核心模块的加载优先原则都是优先从缓存加载
,而且node缓存的是编译和执行之后的对象。
4、require
方法的来源
可以在index.js
文件中执行:
console.log(require)
执行结果为:
可以看出其中包含了属性resolve
、main
、extensions
和cache
属性。
那么,为啥可以在文件中直接使用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
:当前执行文件夹名称。
至此,就解释了require
在node
中的实现。
总结
当我们在文件中输入const math = require('math')
时候,相当于在当前文件模块中执行了模块函数中传入的参数require
,require
包含了其支持的模块引入类型,模块寻找路径和已被缓存的文件模块。
转载自:https://juejin.cn/post/7186299715560931384