深刻浅出Node.js(一) 模块机制

模块分类

Node.js有两种模块javascript

  1. 核心模块
    部分核心模块已经被直接加载进内存中,路径分析 编译执行的步骤能够省略 而且在路径分析中优先被判断,因此加载速度最快
  2. 文件模块
    运行时动态加载,因此须要完整的路径分析 文件定位和 编译执行过程,因此速度比核心模块慢

实现“模块”功能的奥妙就在于JavaScript是一种函数式编程语言,它支持闭包。若是咱们把一段JavaScript代码用一个函数包装起来,这段代码的全部“全局”变量就变成了函数内部的局部变量。java

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

因此,Node利用JavaScript的函数式编程的特性,垂手可得地实现了模块的隔离。编程

模块缓存机制

Nodejs对加载过的模块 会进行缓存,以减小二次引入时的开销,引入模块时会优先从缓存中查找,Node缓存的是编译和执行以后的对象json

缓存形式: 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);

整个详细的过程分析以下:

  1. node main.js
  2. require a.js,load a.js,输出“a starting“
  3. a: exports.done = false,require b.js,load b.js
  4. 输出”b starting“,b: exports.done = false
  5. require a.js, 因为a.js没有执行完,将未完成的副本导出,因此 a = {done: false}
  6. 输出in b, a.done = false
  7. b: exports.done = true,输出b done,b.js执行完毕,返回a.js继续执行
  8. b = { done: true },输出in a, b.done = true,输出a done
  9. a.js 执行完毕,a = { done: true } ,返回 main.js 继续执行,require b.js
  10. 因为 b.js 已经被执行完毕,缓存中拿值,如今 a = { done: true }, b = { done: true }
  11. 输出in main, a.done = true, b.done = true

因而可知,Node.js对已加载过的模块进行缓存,解决了循环引用的问题,在二次加载时直接从缓存中取,提升了加载速度。

路径分析和文件定位

咱们须要了解,自定义模块是动态加载的(运行时加载),在首次加载时,要通过路径分析、文件定位、编译执行的过程。

在分析路径模块时,require()方法会去查找真实的路径

  1. 若是没有扩展名,会按照分析顺序:.js > .node > .json 依次进行匹配
  2. 若是没有查找到对应的文件,可是获得的是一个目录,那么会被当成一个包来处理
    优先查找package.jsonmain属性指定的文件名进行定位 > 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文件时,会生成一个moduleexports对象, module还有一个exports属性,module.exportsexports指向同一个引用
二者的根本区别是:
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 模块机制不彻底指北