详细理解 amd,commonjs,es6module
在开始讲解前,先了解下CommonJS、AMD、ES6模块的侧重点,请仔细阅读此章节。
模块的侧重点
前后端 JavaScript 分别搁置在 HTTP 的两端(后端 JS 指 Node),它们扮演的角色并不同。
浏览器的 JavaScript 需要经历从同一个服务器(Web服务器)端分发到多个客户端执行,而服务器端的 JavaScript 则是相同的代码需要多次执行。前者的瓶颈在于带宽,后者的瓶颈在于 CPU 和内存等资源。前者需要通过网络加载代码,后者从磁盘中加载,两者的加载速度不在一个数量级上。
**纵观 Node 的模块引入过程(CommonJS规范
的 require
),几乎全都是同步的。**尽管与 Node 强调异步的行为有些相反,但它是合理的。前端 JavaScript 与 ui 渲染共用一个线程,如果前端模块也采用同步的方式来引入,将会在用户体验上造成很大问题。UI 在初始化过程中需要话费很多时间来等待脚本加载完成。
鉴于网络原因,CommonJS 为后端 JavaScript 制定的规范并不完全适合前端的应用场景。经过一段争执之后,AMD 规范最终在前端应用场景中胜出。它的全称是 Asynchronous Module Definition,即“异步模块定义”。详见 github.com/amdjs/amdjs… 。除此之外,还有玉伯定义的 CMD 规范。
所以:
对后端来说,同步加载没有问题,因为:
- 模块都在本地,等待时间就是硬盘读取时间。
- 一旦启动之后一般不会关,可靠性比启动时间重要。
对前端来说:
- 模块都在服务器上,需要通过网络请求,太慢。
- 同步 xhr 会堵塞浏览器,假死的话用户体验很差,首屏时间很重要。
CommonJS 规范主要应用在 Node 模块的实现,虽然 Node 并非完全按照其规范实现,但是大体相同。
为了实现前端 JavaScript 的异步加载应使用 AMD 规范。
ES2015 发布之后又出现的新的模块引入方式 ES6 module。**ES6 加载模块并没有指定同步或异步,如何加载 ES6 定义的模块很可能取决于代码的运行环境。**它可成为浏览器和服务器通用的模块解决方案。
AMD 规范
AMD 规范是 CommonJS 模块规范的一个延伸,它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
它的模块定义如下:
define(id?, dependencies?, factory);
它的模块 id 和依赖是可选的,与Node模块相似的地方在于factory的内容就是实际代码的内容。下面的代码定义了一个简单的模块:
define(function(){
var exports = {};
exports.sayHello = function(){
console.log('Hello from module: ' + module.id);
};
return exports;
});
与 CommonJS 不同之处在于 AMD 模块需要用 define
来明确定义一个模块,而在 Node 实现中是隐式包装的,它们的目的是进行作用域隔离,仅在需要的时候被引入,避免掉过去那种通过全局变量或全局命名空间的方式,一面变量污染和不小心被修改。另一个区别则是内容需要通过返回的方式实现导出。
ES6 模块与 CommonJS 模块的差异
ES6 模块 与 CommonJS 模块 有三个重大差异:
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
- CommonJS 模块顶层的
this
指向当前模块,ES6 模块顶层的this
指向undefined
注意⚠️,ES6 模块的顶级(默认)导出 export default 的是值的拷贝,与次级导出的行为不一致。
第二个差异是因为 CommonJS 加载的是一个对象(即module.exports
属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
下面重点解释第一个差异。
CommonJS
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
上面代码说明,lib.js
模块加载以后,它的内部变化就影响不到输出的mod.counter
了。这是因为mod.counter
是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter,
};
上面代码中,输出的counter
属性实际上是一个取值器函数。现在再执行main.js
,就可以正确读取内部变量counter
的变动了。
$ node main.js
3
4
ES6 模块
ES6 模块的运行机制与 CommonJS 不一样。
JS 引擎对脚本静态分析的时候,遇到模块加载命令import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
还是举上面的例子。
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
由于 ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。
// lib.js
export let obj = {};
// main.js
import { obj } from './lib';
obj.prop = 123; // OK
obj = {}; // TypeError
上面代码中,main.js
从lib.js
输入变量obj
,可以对obj
添加属性,但是重新赋值就会报错。因为变量obj
指向的地址是只读的,不能重新赋值,这就好比main.js
创造了一个名为obj
的const
变量。
**即使将 es6 模块通过 babel 等工具编译为 commonjs 模块,那么通过 nodejs 运行后依旧为值的拷贝,并不会表现出 es6 模块本身的特性。 ** 这可能由于浏览器的 js 引擎与 node js引擎存在着差异。
最后看这个例子。
export
通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。
// mod.js
function C() {
this.sum = 0;
this.add = function () {
this.sum += 1;
};
this.show = function () {
console.log(this.sum);
};
}
export let c = new C();
上面的脚本mod.js
,输出的是一个C
的实例。不同的脚本加载这个模块,得到的都是同一个实例。
// x.js
import {c} from './mod';
c.add();
// y.js
import {c} from './mod';
c.show();
// main.js
import './x';
import './y';
现在执行main.js
,输出的是1
。
$ babel-node main.js
1
这就证明了x.js
和y.js
加载的都是C
的同一个实例。
依据 ES6 模块 这个原理,所以我们可以轻松实现 发布订阅的事件池。
参考
-
ES2015标准入门(第3版)— 阮一峰
-
深入浅出NodeJS - 朴灵
-
stackoverflow.com/questions/3…