深入浅出Node.js 模块机制
Posted 高压郭
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入浅出Node.js 模块机制相关的知识,希望对你有一定的参考价值。
模块分类
Node.js有两种模块
- 核心模块
部分核心模块已经被直接加载进内存中,路径分析 编译执行的步骤可以省略 并且在路径分析中优先被判断,所以加载速度最快 - 文件模块
运行时动态加载,所以需要完整的路径分析 文件定位和 编译执行过程,所以速度比核心模块慢
实现“模块”功能的奥妙就在于javascript是一种函数式编程语言,它支持闭包。如果我们把一段JavaScript代码用一个函数包装起来,这段代码的所有“全局”变量就变成了函数内部的局部变量。
var s = \'Hello\';
var name = \'world\';
console.log(s + \' \' + name + \'!\');
(function() {
var s = \'Hello\';
var name = \'world\';
console.log(s + \' \' + name + \'!\');
})()
这样一来,原来的全局变量s现在变成了匿名函数内部的局部变量。如果Node.js继续加载其他模块,这些模块中定义的“全局”变量s也互不干扰。
所以,Node利用JavaScript的函数式编程的特性,轻而易举地实现了模块的隔离。
模块缓存机制
Nodejs对加载过的模块 会进行缓存,以减少二次引入时的开销,引入模块时会优先从缓存中查找,Node缓存的是编译和执行之后的对象
缓存形式: key-value的形式,以真实路径作为key,以编译执行后的结果作为value 放在缓存中(Module._cache对象中)(二次加载速度更快)
打印rquire.cache
可以看到缓存的对象
模块的循环引用
先说结论,由于Node.js会缓存加载过的模块,所有模块的循环依赖并不会引起无限循环引用。举个例子:
a.js
文件下
console.log(\'a starting\');
exports.done = false
const b = require(\'./b.js\')
console.log(\'in a, b done = %j\', b.done);
exports.done = true
console.log(\'a done\');
b.js
文件下
console.log(\'b starting\');
exports.done = false
// 这里导入的是a未执行完的副本
const a = require(\'./a.js\')
console.log(\'in b, a done = %j\', a.done);
exports.done = true
console.log(\'b done\');
main.js
文件下
console.log(\'main starting\');
const a = require(\'./a\')
const b = require(\'./b\')
console.log(\'in main.js, a done = %j, b done = %j\', a.done, b.done);
整个详细的过程分析如下:
- node main.js
- require a.js,load a.js,输出“a starting“
- a: exports.done = false,require b.js,load b.js
- 输出”b starting“,b: exports.done = false
- require a.js, 由于a.js没有执行完,将未完成的副本导出,所以 a = {done: false}
- 输出in b, a.done = false
- b: exports.done = true,输出b done,b.js执行完毕,返回a.js继续执行
- b = { done: true },输出in a, b.done = true,输出a done
- a.js 执行完毕,a = { done: true } ,返回 main.js 继续执行,require b.js
- 由于 b.js 已经被执行完毕,缓存中拿值,现在 a = { done: true }, b = { done: true }
- 输出in main, a.done = true, b.done = true
由此可见,Node.js对已加载过的模块进行缓存,解决了循环引用的问题,在二次加载时直接从缓存中取,提高了加载速度。
路径分析和文件定位
我们需要了解,自定义模块是动态加载的(运行时加载),在首次加载时,要经过路径分析、文件定位、编译执行的过程。
在分析路径模块时,require()
方法会去查找真实的路径
- 如果没有扩展名,会按照分析顺序:.js > .node > .json 依次进行匹配
- 如果没有查找到对应的文件,但是得到的是一个目录,那么会被当成一个包来处理
优先查找package.json
中main
属性指定的文件名进行定位 > index.js > index.node > index.json 依次匹配
模块的编译
编译和执行是引入文件模块的最后一个阶段。这里只讲对.js
文件的编译,通过fs
模块同步读取文件后进行编译,每个编译成功的模块都会以它的真实路径作为索引缓存在Module._cache
对象上,以提高二次引入的性能。
编译过程中,Node会对获取到的文件进行头尾包装
(function(module, exports, require, __filename, __dirname) {
})
这样每个模块之间都进行了作用域隔离 也解释了我们没有在模块文件中定义module、exports、__filename、 __dirname
这些变量却可以使用它们的原因。
module.exports和exports的区别
Node.js在执行一个javascript
文件时,会生成一个module
和exports
对象, module
还有一个exports
属性,module.exports
和exports
指向同一个引用
两者的根本区别是:
exports返回的是模块函数,module.exports返回的是模块对象本身
举个例子:a.js
文件下
let sayHello = function() {
console.log(\'hello\');
}
exports.sayHello = sayHello
b.js
文件下
// 这样使用会报错
const sayHello = require(\'./a\')
sayHi()
// 正确的方式
const func = require(\'./a\')
func.sayHello() // hello
新建c.js
文件
let sayHello = function() {
console.log(\'hello\');
}
// 1方式导出
module.exports.sayHello = sayHello
// 2方式导出
module.exports = sayHello
在b.js
中引入
// 1方式的
const func = require(\'./a\')
func.sayHello() // hello
// 2方式的
const sayHello = require(\'./a\')
sayHello() // hello
可以看出,1方式的导出跟exports的导出,在引入时的方式是一致的
module.exports.sayHello = sayHello
等同于
module.exports = {
sayHello: sayHello
}
也等同于
exports.sayHello = sayHello
还有一个注意的点是:执行require()
方法时,引入的是module.exports
导出的内容。
// d.js文件下:
exports = {
a: 200
}
module.exports = {
a: 100
}
// b.js引入
const value = require(\'./d\')
console.log(\'value\', value); // {a: 100}
从上面可以看出,其实require导出的内容是module.exports的指向的内存块内容,并不是exports的。
// 如果d.js文件变成
exports = {
a: 200
}
// b.js引入
const value = require(\'./d\')
console.log(\'value\', value); // {}
可以看到打印出来的值为{}
那是因为exports
本来指向跟module.exports
同一个引用,现在exports = {a: 200}
exports指向了另一个内存地址,将与module.exports脱离关系,默认module.eports={}
总结
- exports 是 module.exports 的一个引用
- module.exports 初始化是一个{},exports 也是这个{}
- require 引用返回的是 module.exports,而不是 exports
- exports.xxx = xxxx 相当于在导出对象上直接添加属性或者修改属性值,在调用模块直接可见
- exports = xxx 为 exports 重新分配内存,将脱离 module.exports ,两者无关联。调用模块将不能访问。
参考:
Node 模块机制不完全指北
以上是关于深入浅出Node.js 模块机制的主要内容,如果未能解决你的问题,请参考以下文章