一、 为什么需要模块化
在最早做前端开发的时候,我们经常会遇到一个问题,在引入脚本的时候,变量会对全局造成污染,如下:
a.js
function log() {
console.log('a')
return 'a'
}
b.js
function log() {
console.log('b')
return 'b'
}
index.html
<meta charset="utf-8" />
<script src="./a.js"></script>
<script src="./b.js"></script>
<script>
console.log('log: ', log()) // b
</script>
上面的例如,我们发现无论如何都无法调用到a文件的log方法 原因是原始的这种写法会导致log污染了全局,所以特别是大型的项目,引用了较多的第三方库的情况下,问题是比较难排查出来的。所以就一种模块化的解决方案来解决这个问题。
目前市面上的解决方案有AMD,CMD,UMD,CommonJs,ES Module,这里面我们就只介绍其中的CommonJs和ES Module
AMD,CMD,UMD其实是最早的模块化解决方案,代表的类库有Require.js和Sea.js,这里就不多做介绍,因为目前前端的开发以打包工具构建编译后代码为主,主流的打包工具都是支持Commonjs和ES Module所以其他的解决方案不做介绍
二、CommonJs
在node.js出现之后,CommonJs被率先的引入进来解决污染全局环境的问题。
CommonJs是通过require方法来导入模块,通过module.exports和exports来实现导出的
例如:
a.js
module.exports = {
module: 'a'
}
b.js
module.exports = {
module: 'b'
}
index.js
const a = require('./a.js')
const b = require('./b.js')
console.log('a: ', a)
console.log('b: ', b)
运行的结果是分别打印a和b
我们来解析一下源码:
(() => {
var o = {
847: o => {
o.exports = { module: "a" }
},
996: o => {
o.exports = { module: "b" }
}
},
r = {};
function e(t) {
var s = r[t];
if (void 0 !== s)
return s.exports;
var n = r[t] = {
exports: {}
};
return o[t](n, n.exports, e), n.exports
}
(() => {
const o = e(847),
r = e(996);
console.log("a: ", o), console.log("b: ", r)
})()
})();
//# sourceMappingURL=main.js.map
我们可以看到实现的原理是,把所有的代码包装在一个自执行的函数里面,然后通过将导入的模块转化成为一个对象的形式来实现模块化方案
由上面的源码我们知道CommonJs最大的特点是:
- 模块化的方案是动态的,也就是调用的时候引入模块
- CommonJs会缓存之前被调用过的模块
这里补充一下前端关键字void,关键字void修饰之后不返回内容,如上所述,void 0也就是undefined
下面我们将a文件改为:
const getMes = require('./b')
console.log('我是 a 文件')
exports.say = function() {
const message = getMes()
console.log(message)
}
b文件改为:
const say = require('./a')
const object = {
author: '我不是外星人'
}
console.log('我是 b 文件')
console.log('打印 a 模块', say)
setTimeout(() => {
console.log('异步打印 a 模块', say)
}, 0)
module.exports = function() {
return object
}
编译后的源码如下:
(() => {
var o = {
847: (o, n, e) => {
const t = e(996);
console.log("我是 a 文件"),
n.say = function() {
const o = t();
console.log(o)
}
},
996: (o, n, e) => {
const t = e(847),
s = {
author: "我不是外星人"
};
console.log("我是 b 文件"),
console.log("打印 a 模块", t),
setTimeout((() => {
console.log("异步打印 a 模块", t)
}), 0),
o.exports = function() { return s }
}
},
n = {};
function e(t) {
var s = n[t];
if (void 0 !== s)
return s.exports;
var r = n[t] = {
exports: {}
};
return o[t](r, r.exports, e), r.exports
}
e(847), e(996), console.log("node 入口文件")
})();
//# sourceMappingURL=main.js.map![]()
运行之后的结果如下:
-
首先我们会加载a文件(对应的编号是847)
var s = n[t]; if (void 0 !== s) return s.exports; var r = n[t] = { exports: {} }; -
通过上述的加载代码我们可以知道在初次加载A模块的时候,s=undefined,这个时候就会对A模块分配一个含有exports的空对象,接着把空对象的exports属性导出,因为A模块第一行代码调用了b模块,重复加载a模块的步骤的步骤,也就是b模块的say常量是一个空对象(对应的打印结果是:打印a模块{})
-
接着按照顺序进行执行,输出我是b文件,按照顺序接着输出打印a模块{}
-
接着是执行setTimeout宏任务,所以setTimeout会在最后输出
-
最后在b模块中,我们会对其进行一个导出操作,并通过e方法传递的缓存对象进行更新exports操作
-
接着按照顺序分别打印我是a文件和node入口文件
-
最后因为setTimeout是宏任务最后执行,且a模块方法已经被执行exports导出say方法,所以打印:异步打印a模块:{say:Fn}
note:为什么 exports={} 直接赋值一个对象就不可以呢
我们将a文件改为:
const getMes = require('./b')
console.log('我是 a 文件')
exports = {
say: function() {
const message = getMes()
console.log('a 文件 say', message)
}
}
编译之后如下:
从这里我们可以看出,本质上的原因是没有生成导出的源码,这个可能与其他的技术文章有点出入,也就是我想说明的一点,不看源码来做分析多多少少都有点误区。
三、ES Module
Es Module与CommonJs 的本质上的区别是CommonJs是动态加载的,Es Module是静态编译的,这个也就限制了ES Module可以在webpack中进行tree shaking来减少代码,动态执行的CommonJs是不能进行tree shaking。
将a文件改为:
export default {
a: true
}
b文件改为:
export default {
b: true
}
index文件改为:
import a from './a.js'
import b from './b.js'
console.log('a: ', a)
console.log('b: ', b)
编译后的源码如下:
(() => {
"use strict";
console.log("a: ", { a: !0 }), console.log("b: ", { b: !0 })
})();
//# sourceMappingURL=main.js.map
下面我们来探究一下在ES Module中是怎么处理模块相互依赖的
a文件改为:
import b from './b'
console.log('a模块加载')
export default function say() {
console.log('hello , world')
}
b文件改为:
console.log('b模块加载')
export default function sayhello() {
console.log('hello,world')
}
index文件为:
console.log('main.js开始执行')
import say from './a'
import say1 from './b'
console.log('main.js执行完毕')
编译后如下:
(() => {
"use strict";
console.log("b模块加载"), console.log("a模块加载"), console.log("main.js开始执行"), console.log("main.js执行完毕")
})();
//# sourceMappingURL=main.js.map
我们可以得出结论:
- import导入的模块会优先执行
- 没有使用到的代码不会被编译到最终执行的代码里面去
- ES 模块会在预加载的时候分析模块依赖,在执行模块的时候采用深度遍历的形式执行
四、小结
通过从源码的角度去解读CommonJs和ES Module的差别,能够让我们更清晰的理解他们之间的同异点,笔者认为,单纯的阅读网上的长篇大论的文章去了解他们的区别,不利于记忆和应用。所以阅读源码是一种简单而且方便的学习的方法