# CommonJS模块规范
模块引用
var math = require('math');
模块定义 上下文提供exports对象用于导出当前模块的方法和变量,并且他是唯一的导出出口
exports实际上是module.exports,而module.exports就是以一个暴露给外部的对象。
- exports.some就是给这个对象上添加属性
- 直接使用 module.exports = {...} 则可以让外部直接获取到这个对象,相当与为exports换了一个引用,如果在这之前使用exports.some会把之前的覆盖
# CommonJS 用法
// a.js
module.exports = {
a: 1
}
// or
exports.a = 1
// b.js
var module = require('./a.js')
module.a // -> log 1
2
3
4
5
6
7
8
9
10
原理
var module = require('./a.js')
module.a
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
a: 1
}
// module 基本实现
var module = {
id: 'xxxx', // 我总得知道怎么去找到他吧
exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
// 导出的东西
var a = 1
module.exports = a
return module.exports
};
// 然后当我 require 的时候去找到独特的
// id,然后将要使用的东西用立即执行函数包装下,over
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# exports 和 module.exports 的区别
- module.exports 默认值为{}
- exports 是 module.exports 的引用
- exports 默认指向 module.exports 的内存空间
- require() 返回的是 module.exports 而不是 exports
- 若对 exports 重新赋值,则断开了 exports 对 module.exports 的指向
# Node的模块实现
在Node中引入模块,需要经历3个步骤
- 路径分析
- 文件定位
- 编译执行
在node中,模块分为两类:一类是node提供的模块称为核心模块,一类是用户编写的成为文件模块。
- 核心模块在编译中编译成了二进制文件。在Node进程启动时,部分核心模块就被直接加载入内存。所以这部分核心模块引入时就省了文件定位和编译执行这两个步骤,并且在路径分析中优先判断,它的加载速度是最快的。
- 文件模块是运行时动态加载。需要完整的路径分析、文件定位、编译执行
# 优先从缓存加载
Node对引入的模块都回进行缓存,而且缓存的是编译执行后的对象。
不管是核心模块还是文件模块,require()都一律采用缓存优先的方式。
# 路径分析和文件定位
模块标识符分析
- 核心模块
- 路径形式的文件模块
- 自定义模块
- node_modules下
- 查找最费时
文件定位
- 文件拓展名分析
- 如果省略拓展名,回按 .js .node .json的次序依次尝试
- 如果.node .json的话,加上拓展名会加快一点速度
- 同步配合缓存,可大幅缓解单线程中阻塞式调用的缺陷
- 目录分析和包
- 如果没有文件名,会将Index当作默认文件名
# 模块编译
- .js文件
- 通过fs同步读取后编译执行
- .node
- 这是用C/C++编写的拓展文件,通过dlopen()方法加载最后编译生成的文件
- .json
- 用JSON.parse()解析返回结果
- 其余拓展名
- 当作.js文件处理
每一个编译成功的模块都会将其文件路径索引缓存在Module._cache对象上,以提高二次引入性能
# js模块的编译
在编译的过程中,Node对获取的JS文件进行了头尾包装。这也是每个模块都能访问到 require、exports、module、__filename、__dirname的原因
(funciton(exports, require, module, __filename, __dirname) {
/* 自己写的代码 */
});
2
3
4
5
这样使得模块文件间都进行了作用域隔离,不用担心变量污染全局。
为moudle.exports赋值,exports对象是通过形参的方式传入,直接赋值形参会改变形参的引用,但并不能改变作用域外的值。
exports = function() {
// my class
}
var change = function(a) {
a = 100;
}
var a = 10;
change(a);
console.log(a); // => 10
2
3
4
5
6
7
8
9
10
11
如果要达到require引入一个类的效果,请赋值给 module.exports对象。这个迂回的方案不改变形参的引用。
# C/C++ 模块的编译
Node调用process.dlopen()方法进行加载和执行。
实际上 .node模块并不需要编译,因为它是编写C/C++模块之后编译生成的,所以这里只有加载和执行的过程。在执行的过程中,模块exports对象与.node模块产生练习,然后返回给调用者。
# 核心模块
Node的核心模块在编译成可执行文件的过程中被编译进了二进制文件。核心模块其实分为C/C++编写的和Javascript编写的两部分,其中C/C++文件存放在Node项目的src目录下,Javascript文件存放在lib目录下。
# 前后端公用模块
# 模块侧重点
前端瓶颈在于带宽,后端瓶颈在于CPU和内存等资源。前端需要通过网络加载代码,后端则从磁盘加载,二者加载速度不再同一量级上。
node的模块引入几乎都是同步的,但前端模块若是也采用同步方式来引入必会在用户体验上造成很大的问题,即UI初始化实际过长
# AMD
Asynchronous Moudle Definition “异步模块定义” AMD需要在声明的时候指定所有的依赖,通过形参传递依赖到模块内容中。
定义如下
define(id?, dependencies, factory);
# CMD
与AMD主要区别在于定于模块与依赖引入部分。
CMD支持动态引入
define(funtion(require, exports, moudle) {
// The module code goes here
})
2
3
# commonJS 与 ESmodule差异
- commonJs是被加载的时候运行,esModule是编译的时候运行
- commonJs输出的是值的浅拷贝,esModule输出值的引用
- commentJs具有缓存。在第一次被加载时,会完整运行整个文件并输出一个对象,拷贝(浅拷贝)在内存中。下次加载文件时,直接从内存中取值
参考:https://juejin.im/post/5ae04fba6fb9a07acb3c8ac5
# commonJs 输出值拷贝
/*************** a.js**********************/
let count = 0
exports.count = count; // 输出值的拷贝
exports.add = ()=>{
//这里改变count值,并不会将module.exports对象的count属性值改变
count++;
}
/*************** b.js**********************/
const { count, add } = require('./a.js')
//在支持es6模块的环境下等同于
import { count, add } from './a.js'
console.log(count) //0
add();
console.log(count)//0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# esModule 输出值引用
/*************** a.js**********************/
export let count = 0;//输出的是值的引用,指向同一块内存
export const add = ()=>{
count++;//此时引用指向的内存值发生改变
}
/*************** b.js**********************/
import { count, add } from './a.js'
console.log(count) //0
add();
console.log(count)//1
2
3
4
5
6
7
8
9
10
11
12
13
# commonJs 输出的浅拷贝验证
/*************** a.js**********************/
const foo = {
count: 0
}
//module.exports的foo属性为 foo 对象的浅拷贝,指向同一个内存中
exports.foo=foo;
window.setTimeout(()=>{
foo.count += 1
console.log('changed foo')
},1000)
/*************** b.js**********************/
const { foo } = require('./a.js')
console.log('foo', foo);//'foo',{count: 0}
window.setTimeout(()=>{
console.log('after 2s foo', foo);//'after 2s foo ',{count: 1}
}, 2000)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# commonJs 输出时的危险操作
其实上个栗子中的 const { foo } = require('./a.js') 或者 const foo = require('./a.js').foo 写法是相当危险的。因为commonJs输出的值的拷贝,若后面在a.js中 对foo的内存指向作出改动,则不能及时更新。 我们将上面的栗子做个小改动:
/*************** a.js**********************/
const foo = {
count: 0
}
exports.foo=foo; //此时foo指向 {count: 0}的内存地址
window.setTimeout(()=>{
//改变 foo 的内存指向
exports.foo='haha';
},1000)
/*************** b.js**********************/
const { foo } = require('./a.js'); //拷贝了 foo属性指向 {count: 0} 内存地址的引用
console.log('foo', foo);//'foo',{count: 0}
window.setTimeout(()=>{
//看!并没有改变!
console.log('after 2s foo', foo);//'after 2s foo ',{count: 0}
}, 2000)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
改进:
/*************** b.js**********************/
const test = require('./a.js');
//test 拷贝了 整个输出对象{foo:{count: 0} }内存地址的引用
//当内存中的属性值发生变化时,可以拿到最新的值,因为指向的是同一片内存
console.log('foo', test.foo);//'foo',{count: 0}
window.setTimeout(()=>{
//保证获取到的是最新的
console.log('after 2s foo', test.foo);//'after 2s foo ','haha'
}, 2000)
2
3
4
5
6
7
8
9
10
进阶:
/*************** child.js**********************/
let foo = 1
setTimeout(()=>{
foo=2;
exports.foo= foo
},1000)
exports.foo=foo
/*******************index.js***************************/
var test =require('./child');
console.log(test.foo);// 1
setTimeout(()=>{
console.log(test.foo) // 2
},2000)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
将child.js中的输出方式做一下改动,结果就变了。
/*************** child.js**********************/
let foo = 1
setTimeout(()=>{
foo=2;
module.exports={foo};//注意:指向新内存 {foo:2}
},1000)
module.exports={foo}; //指向内存 {foo:1}
/*******************index.js***************************/
var test =require('./child');// 浅拷贝,指向的还是{foo:1}的内存,并缓存在内存中
console.log(test.foo);// 1 //从缓存的内存中取值
setTimeout(()=>{
console.log(test.foo) // 1 //从缓存的内存中取值
},2000)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17