NodeJs中的模块化
require和module.exports使用
在NodeJs中,我们会通过module.exports或者exports来导出一个javascript文件中定义的元素,然后通过require将导出元素进行引入:
// demo02.js
console.log('1');
module.exports = () => {
console.log('Hi, I am module demo2');
};
console.log('2');
// demo01.js
console.log('before require');
const demo2 = require('./demo02');
console.log('after require');
demo2();
接下来我们在当前目录中打开命令行窗口,输入node demo02.js:
before require
1
2
after require
Hi, I am module demo2
所以,我们在通过require将一个模块导入的时候,不仅可以接收模块内部通过module.exports暴露的元素,还会执行相应模块内的js代码
接下来,我们在demo01.js中再加入以下代码:
const repeatDemo2 = require('./demo02');
repeatDemo2();
执行后的输出结果如下:
before require
1
2
after require
Hi, I am module demo2
Hi, I am module demo2
输出结果大概告诉我们这样一件事: 在首次引入某个模块的时候,NodeJs会对模块内的代码进行缓存,而当我们再次引入该模块时,会通过缓存来读取导出的内容,模块内的代码并不会重新执行。
我们可以通过require.cache属性来看到NodeJs对模块的缓存:
// 在引入模块之前和之后分别输出require.cache
// demo03.js
console.log('before require');
console.log(require.cache);
const demo2 = require('./demo02');
console.log('after require');
console.log(require.cache);
通过截图我们可以很明显的看出,在require demo02后缓存中多了一些内容:
在阅读完上边的代码之后,这里我们可以对require的功能进行一个小结:
require会引入一个模块中通过module.exports导出的元素- 在
require首次引入模块过程中,会执行模块文件中的代码,并将模块文件进行缓存 - 当我们再次引入该模块的时候,会从缓存中读取该模块导出的元素,而不会再次运行该文件
exports和module.exports
我们先看一下NodeJs官方对exports的定义:
exports变量是在模块的文件级作用域内可用的,且在模块执行之前赋值给module.expors
这句话的大概意思是说: exports并不是一个全局变量,只在模块文件内有效,并且在每个模块文件(js文件)执行之前将module.exports的值赋值给exports。即相当于在每个js文件的开头执行了如下代码:
exports = module.exports
这意味着exports和module.exports指向了同一片内存空间,当为exports或者module.exports重新赋值的时候,它们将不再指向同一个引用,而我们requie引入的一直都是module.exports导出的内容。
// demo04.js
// 本质上来讲:exports是module.exports的一个引用,它们指向同一片内存空间
// exports = module.exports
exports.a = 1;
module.exports = { b: 2 }; // 当引用发生变化的时候,exports不再是module.exports的快捷方式
这时模块暴露出来的对象是{b:2}。
官方也对这种行为进行了假设实现:
function require(/* ... */) {
// 一个全局的module对象
const module = { exports: {} };
// 这里自执行函数传参时进行了赋值: exports = module.exports
((module, exports) => {
// 模块代码在这。在这个例子中,定义了一个函数。
function someFunc() {}
exports = someFunc;
// 此时,exports 不再是一个 module.exports 的快捷方式,
// 且这个模块依然导出一个空的默认对象。
module.exports = someFunc;
// 此时,该模块导出 someFunc,而不是默认对象。
})(module, module.exports);
// 最终导出的一直都是module.exports,只不过可以通过exports来更改它们的引用,间接的改变module.exports
return module.exports;
}
模块之间的循环引用
假设我们有这样一种场景: 模块a.js依赖于b.js中的某个方法,而模块b.js也同样依赖于a.js中的某个方法,这样的话会不会造成死循环呢?
笔者这里写了一个demo来重现这个问题,帮助我们更好的理解模块之间的相互引用:
// demo05.js
const demo6 = require('./demo06');
console.log('I am demo5', demo6);
module.exports = { demo5: 'demo5' };
// demo06.js
const demo5 = require('./demo05');
console.log('I am demo6', demo5);
module.exports = { demo6: 'demo6' };
执行结果如下
I am demo6 {}
I am demo5 { demo6: 'demo6' }
所以我们可以得出以下执行过程:
- 命令行执行
node demo05.js - 首先引入模块
demo06.js,并且执行demo06.js,通过变量demo6来接收模块demo06.js通过module.exports导出的对象 - 在执行
demo06.js的过程中,又引入了demo05.js,而由于demo05.js已经执行了一部分,由于缓存原因,并不会重新执行,此时demo05.js中的module.exports还是初始值{}。所以变量demo5为{}。 demo05.js在引入demo06.js后继续执行后续代码
可以看出nodeJs对于模块之间的递归引用进行了优化,并不会引发死循环,但是需要注意的是在引入的时候要注意代码的执行顺序,否则可能会取不到对应的变量。