CommonJS和ES6模块


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

commonjs的用法,我们一起来看一下

1.首先创建一个lib.js的文件

1
2
3
4
5
6
7
8
9
10
11
// lib.js
const counter = 3;
const incCounter = ()=>{
counter++
}

module.exports = {
counter,
incCounter
}

2.再次创建一个main.js,使用commonjs的方式导入

1
2
3
4
5
6
// main.js
var lib = require('./lib');
console.log(lib)
console.log(lib.counter); // 3
lib.incCounter();
console.log(lib.counter); // 3

lib.js模块加载以后,它的内部变化就影响不到输出的lib.counter了。这是因为mod.counter是一个原始类型的值,会被缓存;

esmodule的用法,我们一起来看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
// lib.js
export let counter = 3;
export function incCounter () {
counter++;
}

// main.js
import { counter, incCounter } from './util.mjs'

console.log(counter); //3
incCounter()
console.log(counter) //4

ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。

补充:通过esmodule导入的变量是不能重新赋值修改的

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

1
2
3
4
5
6
7
8
// CommonJS模块
let { stat, exists, readFile } = require('fs');

// 等同于
let _fs = require('fs');
let stat =_fs.stat;
let exists = _fs.exists;
let readfile =_fs.readfile;

上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。因此commonjs属于再运行时才会加载模块的方式。

1
import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高;

三、CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段

同步加载:所谓同步加载就是加载资源或者模块的过程会阻塞后续代码的执行;
异步加载:不会阻塞后续代码的执行;

我们来看一个案例,创建如下的目录;

1
2
3
| -- a.js
| -- index.js
| -- c.js
1
2
3
4
5
6
7
8
9
10
11
// a.js
console.log('a.js文件的执行');
const importFun = () => {
console.log(require('./c').c);
}

importFun()

module.exports = {
importFun
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// index.js
const A = require('./a');
console.log('index.js的执行');

// c.js
console.log('c.js的运行');

const c = 3

module.exports = {
c
}

执行命令 node index.js

1
2
3
4
// a.js文件的执行
// c.js的运行
// 3
// index.js的执行

我们会发现,require的内容会阻塞后续代码的执行。因为c.js先打印出来,然后在是index.js的打印,所以说require()是同步加载的;

1
2
3
4
5
6
7
8
9
10
// a.js
console.log('a.js文件的执行');
export const importFun = () => {
import('./c.js').then(({c})=>{
console.log(c)
})
}

importFun()

1
2
3
4
5
6
7
8
9
// index.js
import {importFun} from './a.js'
console.log('index.js的执行');

// c.js
console.log('c.js的运行');

export const c = 3

1
2
3
4
5
// 结果
// a.js文件的执行
// index.js的执行
// c.js的运行
// 3

可以看的出来:import()是异步加载资源的,因为c.js是在index.js的后面打印出来的,并不会阻塞后续代码的执行;

总结:以上便是commonjs和esmodule的几个区别

1: CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
2: CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
3: CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段

Babel

鉴于浏览器支持度的问题,如果要使用 ES6 的语法,一般都会借助 Babel,可对于 import 和 export 而言,只借助 Babel 就可以吗?

让我们看看 Babel 是怎么编译 import 和 export 语法的。

1
2
3
4
5
6
// ES6
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Babel 编译后

'use strict';

Object.defineProperty(exports, "__esModule", {
value: true
});
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

exports.firstName = firstName;
exports.lastName = lastName;
exports.year = year;

是不是感觉有那么一点奇怪?编译后的语法更像是 CommonJS 规范,再看 import 的编译结果:

1
2
3
4
5
6
7
// ES6
import {firstName, lastName, year} from './profile';
复制代码
// Babel 编译后
'use strict';

var _profile = require('./profile');

你会发现 Babel 只是把 ES6 模块语法转为 CommonJS 模块语法,然而浏览器是不支持这种模块语法的,所以直接跑在浏览器会报错的,如果想要在浏览器中运行,还是需要使用打包工具将代码打包。

webpack

Babel 将 ES6 模块转为 CommonJS 后, webpack 又是怎么做的打包的呢?它该如何将这些文件打包在一起,从而能保证正确的处理依赖,以及能在浏览器中运行呢?

首先为什么浏览器中不支持 CommonJS 语法呢?

这是因为浏览器环境中并没有 module、 exports、 require 等环境变量。

换句话说,webpack 打包后的文件之所以在浏览器中能运行,就是靠模拟了这些变量的行为。

那怎么模拟呢?

我们以 CommonJS 项目中的 square.js 为例,它依赖了 multiply 模块:

1
2
3
4
5
6
7
8
9
console.log('加载了 square 模块')

var multiply = require('./multiply.js');

var square = function(num) { 
return multiply.multiply(num, num);
};

module.exports.square = square;

webpack 会将其包裹一层,注入这些变量:

1
2
3
4
5
6
7
8
9
10
function(module, exports, require) {
console.log('加载了 square 模块');

var multiply = require("./multiply");
module.exports = {
square: function(num) {
return multiply.multiply(num, num);
}
};
}

那 webpack 又会将 CommonJS 项目的代码打包成什么样呢?我写了一个精简的例子,你可以直接复制到浏览器中查看效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 自执行函数
(function(modules) {

// 用于储存已经加载过的模块
var installedModules = {};

function require(moduleName) {

if (installedModules[moduleName]) {
return installedModules[moduleName].exports;
}

var module = installedModules[moduleName] = {
exports: {}
};

modules[moduleName](module, module.exports, require);

return module.exports;
}

// 加载主模块
return require("main");

})({
"main": function(module, exports, require) {

var addModule = require("./add");
console.log(addModule.add(1, 1))

var squareModule = require("./square");
console.log(squareModule.square(3));

},
"./add": function(module, exports, require) {
console.log('加载了 add 模块');

module.exports = {
add: function(x, y) {
return x + y;
}
};
},
"./square": function(module, exports, require) {
console.log('加载了 square 模块');

var multiply = require("./multiply");
module.exports = {
square: function(num) {
return multiply.multiply(num, num);
}
};
},

"./multiply": function(module, exports, require) {
console.log('加载了 multiply 模块');

module.exports = {
multiply: function(x, y) {
return x * y;
}
};
}
})
1
2
3
4
5
6
7
最终的执行结果为:

加载了 add 模块
2
加载了 square 模块
加载了 multiply 模块
9

参考链接

CommonJS和ES6模块有什么区别!
ES6 系列之模块加载方案