首先,我们先看看CommonJS规范,以及CommonJS模块的加载规则。
CommonJS规范
CommonJS规范加载模块是同步的,只有加载完成,才能执行后面的操作。
CommonJS规范中的module、exports和require
- 每个文件就是一个模块,有自己的作用域。每个模块内部,module变量代表当前模块,是一个对象,它的exports属性(即module.exports)是对外的接口。
- module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
- 为了方便,Node为每个模块提供一个exports变量,指向module.exports。
let exports = module.exports
- require命令用于加载模块文件。
CommonJS加载原理:
CommonJS模块就是一个脚本文件,require命令第一次加载该脚本时就会执行整个脚本,然后在内存中生成该模块的一个说明对象。
{
id: '', //模块名,唯一
exports: { //模块输出的各个接口
...
},
loaded: true, //模块的脚本是否执行完毕
...
}
以后用到这个模块时,就会到对象的exports属性中取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存中取值。
CommonJS模块是加载时执行,即脚本代码在require时就全部执行。一旦出现某个模块被“循环加载/引用”,就只输出已经执行的部分,没有执行的部分不会输出。
ES6模块的加载规则
ES6模块与CommonJS有本质区别,ES6模块对导出变量,方法,对象是动态引用,JS 引擎对脚本静态分析的时候,遇到模块加载命令
import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
ES6 Module 和CommonJS Module的区别
- 因为
CommonJS的require语法是同步的,所以就导致了CommonJS模块规范只适合用在服务端,而ES6模块无论是在浏览器端还是服务端都是可以使用的,但是在服务端中,还需要遵循一些特殊的规则才能使用 ; CommonJS模块输出的是一个值的拷贝,而ES6 模块输出的是值的引用;CommonJS模块是运行时加载,而ES6 模块是编译时输出接口,使得对JS的模块进行静态分析成为了可能- 因为两个模块加载机制的不同,所以在对待循环加载的时候,它们会有不同的表现。
CommonJS遇到循环依赖的时候,只会输出已经执行的部分,后续的输出或者变化,是不会影响已经输出的变量。而ES6模块相反,使用import加载一个变量,变量不会被缓存,真正取值的时候就能取到最终的值; - 关于模块顶层的this指向问题,在
CommonJS顶层,this指向当前模块;而在ES6模块中,this指向undefined; - 关于两个模块互相引用的问题,在ES6模块当中,是支持加载
CommonJS模块的。但是反过来,CommonJS并不能requireES6模块,在NodeJS中,两种模块方案是分开处理的。
对于两者的循环引用,从案例上直观来看。
CommonJS中的循环引用案例
//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执行完毕!')
//main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在main.js中,a.done = %j, b.done = %j', a.done, b.done);
依CommonJS规范(node main.js),运行结果为:
在b.js中,a.done = false
b.js执行完毕!
在a.js中,b.done = true
a.js执行完毕!
在main.js中,a.done = true, b.done = true
JS代码执行顺序如下:
1)main.js中先加载a.js,a脚本先输出done变量,值为false,然后加载b脚本,a的代码停止执行,等待b脚本执行完成后,才会继续往下执行。
2)b.js执行到第二行会去加载a.js,这时发生循环加载,系统会去a.js模块对应对象的exports属性取值,因为a.js没执行完,从exports属性只能取回已经执行的部分,未执行的部分不返回,所以取回的值并不是最后的值。
3)a.js已执行的代码只有一行,exports.done = false;所以对于b.js来说,require a.js只输出了一个变量done,值为false。往下执行console.log('在b.js中,a.done = %j', a.done);控制台打印出:在b.js中,a.done = false
4)b.js继续往下执行,done变量设置为true,console.log('b.js执行完毕!'),等到全部执行完毕,将执行权交还给a.js。此时控制台输出:b.js执行完毕!
5)执行权交给a.js后,a.js接着往下执行,执行console.log('在a.js中,b.done = %j', b.done);控制台打印出:在a.js中,b.done = true
6)a.js继续执行,变量done设置为true,直到a.js执行完毕。 a.js执行完毕!
7)main.js中第二行不会再次执行b.js,直接输出缓存结果。最后控制台输出:在main.js中,a.done = true, b.done = true
ES6 Module案例说明:
//even.js
import {odd} from './odd';
var counter = 0;
export function even(n){
counter ++;
console.log(counter);
return n == 0 || odd(n-1);
}
//odd.js
import {even} from './even.js';
export function odd(n){
return n != 0 && even(n-1);
}
//index.js
import * as m from './even.js'; // 1
var x = m.even(5); // 2
console.log(x); // 3
var y = m.even(4); // 4
console.log(y); // 5
依ES6 Module(babel-node index.js)执行结果:
1
2
3
false
4
5
6
true
解析
- ES Module的加载时不执行,换句话说, 在代码1处,声明了一个变量m指向even模块,even加载在内存中,函数没有调用并不会执行。
- 代码2通过m调用even函数,进而声明odd指向odd模块,odd加载在内存中,等待被调用。
- 执行odd,even函数中的代码,打印count,最后返回false
- 代码4同理。