Skip to content

JS那些事之CommonJS、AMD、CMD、ES6的import和循环引用 #3

@wangzianan

Description

@wangzianan

CommonJS

CommonJS 是服务端使用的 JavaScript 模块化规范,使用 require 导入模块,使用 module.exports 将当前模块导出。

API

CommonJS 有三个 API 定义模块:

  1. require 导入
  2. exports 导出
  3. module 模块本身

实例

// main.js
var foo = require("./foo.js")
foo.sayHi();
// foo.js
var sayHi = function(){
	console.log("hello world");
}

exports.sayHi = sayHi;

注意: CommonJS 是同步加载文件,所以比较适合服务端的场景

AMD

全称是 Asynchronous Module Definition,从名字上就可以看出,这种模块是异步的。它更适用于浏览器端的场景,因为服务端加载模块主要是通过磁盘,速度比较快,而浏览器端加载模块主要是通过网络,受限于于带宽,可能比较慢,如果依然是采用同步的方式,那么整个应用就会卡住,因此,异步的模块加载方式更适用于浏览器端。这也是 AMD 诞生的初衷。

API

define(id?,dependencies?,factory);
id - string
dependencies - Array
factory - Function | Object
从 define 的签名可以看出 id 和 dependencies 是可选的。

factory

从 API 定义可以看出模块的 factory 可以是 function 和 object。如果是 function, 模块的 loader 会在适当的时机执行 function。
factory 是 function 主要有以下两点好处:

  1. 模块可以有一个独立运行的环境,只把部分 API 暴露出来
  2. 可以更好的控制模块的初始化过程

依赖声明

AMD 的依赖声明主要有两种方式:

  1. 在 dependencies 声明依赖
  2. 在 factory 中 require 依赖

1.在 dependencies 声明依赖

在 dependencies 中声明参数,然后可以在 factory 中按顺序引用参数。例如
define(["a","b"], function(a, b){})
需要注意的是 dependencies 中会有三个默认参数,"require","exports","module",如果 dependencies 没有没有指定,factory 会接受这三个参数。

2. 在 factory 中 require 依赖

这种这是在用到依赖的地方再去 require,更符合编程的习惯。而且依赖模块比较多的情况下写起来也相对美观一些。这种情况下 factory 的参数默认为 require, exports, modules 。但是真正用到的只有 require,所以其他的可以不用写啦。

define(function(require){
	var a = require("a")
	return {
		sayHi : a.sayHi
	}
})

两种方式的对比

方式2更美观,更符合编程习惯。但是方式二的话需要进行正则分析,比较消耗时间,性能上是问题。折中的做法是用方式二书写,然后打包的时候把它转成方式1

循环引用问题

如果定义了一个模块 a,a 中需要引用模块 b,而 b 中又在引用 a ,就会产生循环引用问题。

// a.js
define(function(require){
	var b = require("b")

	return {
		sayHi: "hi",
		doSomething: function(){}
	}
})
// b.js
define(function(require){
	var a = require("a")
	a.sayHi();
})

这个肯定会报错,因为 b 在 a 还没有 load 完成就开始调用 a 的方法了
image
看了 require 的官方文档,提示当出现循环引用问题,首先应该去思考是不是设计上出了问题。那么,是不是所有循环引用的场景都是不合理的呢,这也未必,本文先不讨论循环引用存在的必要性问题。令人高兴的是,循环引用也是有解。上述的代码只需简单调整下,就能正常运行。

define(function(require){
	return {
		j: function(){
			require("a").sayHi()
		}
	}
})

这样就不会报错了。想知道原理吗?哈哈,不想知道的话就没必要继续读下去了。
再说明原理的时候,首先要弄清楚两个概念,这是我从一个百度大哥写的文章里看到的,装载时依赖运行时依赖

    1. 装载时依赖- 模块在初始化过程就需要用到的依赖模块,我们认为这种依赖是装载时依赖
    1. 运行时依赖- 模块在初始化过程不需要用到,但是在后续的运行过程中需要用到的依赖模块,我们认为这种依赖是运行时依赖
      对于这个环,只要双方都是运行时依赖,这个环就是活的,就能跑下去。如果有一边是装载时依赖,并且在另一个模块还没加载完成就开始调用它的方法,显然是会出错的。
      上述例子中的模块 b,最开始 a 相对于 b 来说是装载时依赖,这时 a 还没加载完全,所以报错了。然后把 a 调整为运行时依赖,就可以运行了。

CMD 介绍

CMD 是 Sea.js 在推广的时候产生的规范,是阿里的 玉伯 提出来。
它们的区别如下:
作者:玉伯
链接:https://www.zhihu.com/question/20351507/answer/14859415
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  1. 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.
  2. CMD 推崇依赖就近,AMD 推崇依赖前置。
    看代码:
// CMDdefine(function(require, exports, module) {   
var a = require('./a')   a.doSomething()   
// 此处略去 100 行  
var b = require('./b') 
// 依赖可以就近书写  
b.doSomething()   
// ... })

// AMD 默认推荐的是
define(['./a', './b'], function(a, b) {  
// 依赖必须一开始就写好    
a.doSomething()    
// 此处略去 100 行    
b.doSomething()    
...}) 

虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。
3. AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。
4. 还有一些细节差异,具体看这个规范的定义就好,就不多说了。

虽然 AMD 和 CMD 前期有一些差异,但在 AMD 后续的版本中已经逐渐填补了。

ES6 的 export、import

ES 6 import 和 export 是从语言层面对模块进行支持,相对于以上的规范, ES 6 的模块是静态的,使得在编译的时候就可以对模块的依赖关系进行分析。
在 ES6 中一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。
export 命令用于规定模块的对外接口。
import 命令用于输入其他模块提供的功能。

参考资料

  1. https://en.wikipedia.org/wiki/CommonJS
  2. http://efe.baidu.com/blog/dissecting-amd-what/
  3. http://www.requirejs.org/docs/api.html#circular
  4. https://www.zhihu.com/question/20351507
  5. CMD 模块定义规范 seajs/seajs#242
  6. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/import

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions