Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CommonJS、AMD/CMD、ES6 Modules 以及 webpack 原理浅析 #28

Open
muwoo opened this issue Jun 6, 2018 · 5 comments
Open

CommonJS、AMD/CMD、ES6 Modules 以及 webpack 原理浅析 #28

muwoo opened this issue Jun 6, 2018 · 5 comments

Comments

@muwoo
Copy link
Owner

muwoo commented Jun 6, 2018

CommonJS

Node.js是commonJS规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。实际使用时,用module.exports定义当前模块对外输出的接口,用require加载模块。

// 定义模块 area.js
function area(radius) {
  return Math.PI * radius * radius;
}

// 在这里写上需要向外暴露的函数、变量
module.exports = { 
  area: area
}

// 引用自定义的模块时,参数包含路径
var math = require('./math');
math.area(2);

但是我们并没有直接定义 module、exports、require这些模块,以及 Node 的 API 文档中提到的__filename、__dirname。那么是从何而来呢?其实在编译的过程中,Node 对我们定义的 JS 模块进行了一次基础的包装:

(function(exports, require, modules, __filename, __dirname)) {
  ...
})

这样我们便可以访问这些传入的arguments以及隔离了彼此的作用域。CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。

{
  id: '...',
  exports: { ... },
  loaded: true,
  ...
}

以后需要用到这个模块的时候,就会到exports属性上面取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。commonJS用同步的方式加载模块,只有在代码执行到require的时候,才回去执行加载。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。

AMD

AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。AMD 规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。说了这么多,来看一下一个AMD规范的RequireJS 是如何定义的:

// 定义 moduleA 依赖 a, b模块
define(['./a','./b'],function(a,b){
   a.doSomething()
   b.doSomething()
}) 

// 使用
require(['./moduleA'], function(moduleA) {
  // ...
})

CMD

CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。比如require.js在申明依赖的模块时会在第一之间加载并执行模块内的代码,而CMD则是在使用的时候就近定义:

define(function(require, exports, module) {
  var a = require('./a')
  a.doSomething()
  var b = require('./b')
  b.doSomething()
})

代码在运行时,首先是不知道依赖的,需要遍历所有的require关键字,找出后面的依赖。具体做法是将function toString后,用正则匹配出require关键字后面的依赖。显然,这是一种牺牲性能来换取更多开发便利的方法。而 AMD 是依赖前置的,换句话说,在解析和执行当前模块之前,模块作者必须指明当前模块所依赖的模块。代码在一旦运行到此处,能立即知晓依赖。而无需遍历整个函数体找到它的依赖,因此性能有所提升,缺点就是开发者必须显式得指明依赖——这会使得开发工作量变大,比如:当你写到函数体内部几百上千行的时候,忽然发现需要增加一个依赖,你不得不回到函数顶端来将这个依赖添加进数组。

ES6 Module

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

import a from './a'
import b from './b'

a.doSomething()
b.doSomething()

function c () {}

export default c

ES6 Modules不是对象,import命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能。

ES6 模块与 CommonJS 模块的差异

CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

  1. CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
  2. ES6 Modules 的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的 import 有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

  1. 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
  2. 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”

CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

参考文章

前端模块化:CommonJS,AMD,CMD,ES6

@muwoo muwoo changed the title AMD、CMD 规范以及 webpack 原理浅析 CommonJS、AMD/CMD、EsModules 以及 webpack 原理浅析 Jun 6, 2018
@muwoo muwoo changed the title CommonJS、AMD/CMD、EsModules 以及 webpack 原理浅析 CommonJS、AMD/CMD、ES6 Modules 以及 webpack 原理浅析 Jun 6, 2018
@ThenMorning
Copy link

通俗易懂。

@guimeisang
Copy link

讲的最清楚的文档

@yrl
Copy link

yrl commented Nov 9, 2018

学习了。

@keer3
Copy link

keer3 commented Mar 3, 2019

(function(exports, require, modules, __filename, __dirname)) {

是module

@caiji-programer
Copy link

CommonJS 模块输出的也是值的引用,模块内部的变化会影响到这个值的!nodejs里加载过的模块会缓存到Module._cache里面的,楼主可以看看node的require的实现。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants