# 模块机制
JavaScript 模块加载类型
CommonJS: 显著的特点就是模块是加载是同步的,就目前来说,受限于宽带速度,并不适用于浏览器中的 JavaScript,NodeJS 用的就是这种
AMD: "异步模块定义",它采用异步方式加载模块,模块的加载不影响它后面语句的运行。依赖这个模块的代码定义在一个回调函数中, 等到加载完成之后,这个回调函数才会运行。RequestJS 就是 AMD规范的一种实现
Node 模块管理是使用的 CommonJS 规范
require 的模块加载机制
计算模块的绝对路径
如果缓存中有该模块,则从缓存中取出该模块
按优先级依次寻找并编译执行模块,将模块推入缓存(require.cache)中
输出模块的 exports 属性
# 模块引入
在 Node 引入模块时,需要经过三个步骤:
解析路径
文件定位
编译执行
TIP
- node 对引入过的模块都会缓存处理,下次再引用相同模块将会从缓存中直接获取,这种缓存是基于文件路径定位的,这表示即使两个相同的文件,但它们位于不同的路径下,也会缓存维持两份
- 要注意的是 node 缓存的不是文件路径而是模块编译和执行后的结果
# 解析路径
node 根据模块类型的不同将使用不同的解析方法,模块类型大致分为三种:内置模块、自己定义模块、第三方模块
内置模块
内置模块如 http
、path
等模块在 Node 源代码编译已经被编译成了二进制代码,所以其加载速度最后
自定义模块
自定义模块就是项目中自己编写的模块,加载这类模块往往需要我们在模块名添加相对路径或绝对路径
const utils = require('./utils/index.js')
第三方模块
一般是指我们使用 npm/yarn
下载到的模块,这类模块会被下载到 node_modeuls
目录,项目中加载这类的模块时也不需要我们添加具体路径,因为 node 默认会从当前文件所在路径开始不断寻找 node_modeuls
文件夹,再从 node_modeuls
文件夹找对应的模块
# 文件定位
经过路径解析就可以找到对应的模块文件了,但是回想一下平时引入的模块的时候有两个细节:
不需要指时具体的文件扩展名
这是因为 Node 会按
.js
,.json
,.node
的次序补充扩展名,依次尝试即使我们引用的模块名刚好只到目录名的程度, Node 也可以正确找到可执行的模块文件
这是因为当 Node 找到与模块名相同的目录时,会在当前目录下查找
package.json
文件,然后通过JSON.parse()
解析描述对象,再从中取出main
字段,根据main
字段来定位最终的文件名如果缺少
main
字段,或者main
指定的文件名错误又或者没有package.json
文件,那么 Node 会将index
当做默认的文件名,也就是会尝试依次查找index.js
、index.json
、index.node
TIP
虽然 Node 会默认补充扩展名,但是 Node 在尝试加载的过程中都需要调用 fs
模块同步阻塞式得判断文件是否存在,所以在加载非 .js
的模块时,最好还是加上具体的扩展名,可以提高一点加载速度
# 模块编译
看下面模块代码:
const path = require('path');
exports.getP = function (){
return path.join(__dirname)
}
在编写 Node 模块时,我们可以使用 requre
、exports
、__dirname
、__filename
等方法和变量,这么方法和变量是从哪来的?
事实上在编译的过程中,Node 对模块代码进行包装,例子上面的例子将转变成以下代码:
(function (exports, reuiqre, module, __filename, __dirname){
const path = require('path');
exports.getP = function (){
return path.join(__dirname)
}
})(exports, reuiqre, module, __filename, __dirname)
通过这样方式传递了内置的方法和变量,同时实现了模块之间的相互隔离
# require的隐患
当require
加载一个模块时,模块内部的代码都会被调用,有时候这可能会带来隐藏的 bug
function test(){
setInterval(function(){
console.log("test")
}, 1000)
}
test()
module.exoprts = test
当 require
这个文件的时候,模块内的定时器将被运行
# 模块化和作用域
模块中的 this
指向 module.exports
var a = 10
console.log(this.a) // undefined
console.log(global.a) //undefined
a = 10
console.log(global.a) // 10
this.a = 10
console.log(module.exports) // { a: 10 }
# CommonJS 模块的循环加载
CommonJS 模块的重要特性是加载时执行,即脚本代码在 require
的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出
// a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
// b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
上面代码之中,a.js
脚本先输出一个 done
变量,然后加载另一个脚本文件 b.js
。注意,此时 a.js
代码就停在这里,等待 b.js
执行完毕,再往下执行
b.js
执行到第二行,就会去加载 a.js
,这时,就发生了“循环加载”。系统会去 a.js
模块对应对象的 exports
属性取值,可是此时 a.js
只执行了 exports.done = false
,所以从 a.js
只输入一个变量 done
,值为 false
然后,b.js
接着往下执行,等到全部执行完毕,再把执行权交还给 a.js
。于是,a.js
接着往下执行,直到执行完毕
# 思考
为什么Node中,request()
加载模块是同步而非异步?
么有标准答案
Node 会自动缓存加载过的模块,而且本地IO开销几乎可以忽略
Node 程序运行在服务端,很少频繁的重启,同步加模块也花不了多少时间