模块化commonJS、AMD、CMD和es6import

98 阅读5分钟

考察点:

  • CommonJS规范是什么?"模块"指的是什么单元?(函数?代码段?文件?文件夹?npm包?)
  • AMD规范与CMD规范是什么?其对应的实现是什么?为什么要提出AMD与CMD?
  • AMD与CMD的区别?
  • CommonJS规范的require与ES6 module规范的import有什么不同?

面试题

循环依赖问题

以下代码的执行结果为何?原理?

// 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);
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';

// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';

CommonJS规范

内容:

  • 规范规定文件为模块的基本单位
  • 规定了4个变量的实现:
    • module[object]: 每个模块内部,module变量代表当前模块。模块有自己的作用域,在模块中定义的变量、函数、类都是私有的,其他模块不可见。(通过立即执行函数实现-闭包特性)
    • exports: 模块对外的接口,加载模块实际上是加载模块的exports属性
    • require: 用于加载模块,模块可以多次加载,但是只会在第一次加载时运行一次(浅复制),然后运行结果就会被缓存,以后再加载模块,就会直接读取缓存结果。想让模块再次运行,必须清除缓存。
    • global: 全局对象,所有模块都可以进行访问。
  • 规范加载模块是同步的
  • 模块加载输出的时值的浅拷贝,内部的改变不影响输出的值
  • 文件加载机制:
    • 用于加载文件时,默认后缀名为'.js',找不到时,会以'.json','.node'再去搜索
    • 若路径以'/'开头,则表示加载绝对路径文件(程序启动路径)
    • 若路径以'.'开头,则表示相对路径
    • 若路径以其他字符开头,则表示加载的是默认提供的核心模块(node_modules中的模块,向上各级查找)
  • 目录加载规则
    • 加载目录时,会自动查看该目录的package.json文件,然后加载文件下main字段指定的入口文件
    • 若该文件没有package.jsonmain字段,则会加载目录下的index.jsindex.node文件
  • 模块的缓存: 缓存的模块保存在require.cache中,可以使用delete require.cache[moduleName]清除缓存
  • 循环依赖的加载: 当发生循环加载时,会先加载不完成的版本,以便加载能往下继续进行
interface module {
	id: number[模块的标识符,通胀是带有绝对路径的模块文件名],
	filename: string[模块的文件名,带有绝对路径],
	loaded: boolean[表示模块是否已经完成加载],
	parent: object<any>[调用该模块的模块,可以用途判断是否为根模块,或使用require.main判断],
	children: object<any>[模块依赖的其他模块],
	exports: object<any>[模块对外输出的值]
}

加载值为浅拷贝(例子也可以使用setTimeout实现)

// a.js
var counter = 1;
var countObj = { value: 1 }
function incCounter() {
  counter++;
  countObj.value ++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
  countObj: countObj,
};

// main.js
var counter = require('./lib').counter;
var counterObj = require('./lib').countObj;
var incCounter = require('./lib').incCounter;

console.log(counter, countObj.value);  // 1 1
incCounter();
console.log(counter, countObj.value); // 1 2

循环加载:

// a.js
exports.x = 'a1';
console.log('a.js ', require('./b.js').x);
exports.x = 'a2';

// b.js
exports.x = 'b1';
console.log('b.js ', require('./a.js').x);
exports.x = 'b2';

// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);

=>

$ node main.js
b.js  a1
a.js  b2
main.js  a2
main.js  b2
main.js  a2
main.js  b2

参考资料:

AMD规范

由来: 由于commonJS规范主要用于node实现,而node端,模块一般是储存在本地的,所以读取和执行较快,而在浏览器端,则不太可能,所以需要异步的加载模块后再调用相关实现。 解决问题:

  • 多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器
  • js加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长 实现: require.js、Dojo.js
/** 网页中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>

/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",  //实际路径为js/lib/jquery.min.js
    "underscore": "underscore.min",
  }
});

// 执行基本操作
require(["jquery","underscore"],function($,_){
  // some code here
});

// 定义一个依赖underscore.js的模块
define(['underscore'],function(_){
  var classify = function(list){
    _.countBy(list,function(num){
      return num > 30 ? 'old' : 'young';
    })
  };
  return {
    classify :classify
  };
})

CMD规范

实现: sea.js 与ADM的差别: AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。

// 定义模块 math.js
define(function(require, exports, module) {
    var $ = require('jquery.js');
    var add = function(a,b){
        return a+b;
    }
    exports.add = add;
});
// 加载模块
seajs.use(['math.js'], function(math){
    var sum = math.add(1+2);
});

ES6 module规范

ES6 在语言标准的层面上,实现了模块功能,旨在成为浏览器和服务器通用的模块解决方案。 其由export与import命令组成,其大部分功能类似于commonJS。 详细资料

ES6 module 与 commonJS 的区别

  1. 语法不同,详细参见具体语法。
  2. commonJS输出的是一个值的拷贝,而ES6 输出的是值的引用。
  • CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值
  • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块
  1. commonJs模块是运行时加载,而ES6 是编译时输出接口
  • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
  • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。

其他参考: