如果没有模块化,很容易出现全局污染和依赖管理混乱的问题。其中,全局污染是指script 内部的变量是可以相互污染的,比如两个脚本中定义了重名的变量,那么这个变量就会被污染。依赖管理混乱是指当多个js脚本之间存在依赖关系,那么只有下层 js 能调用上层 js 的方法,但是上层 js 无法调用下层 js 的方法。为解决上述这两个问题,就出现了模块化。模块化两个重要的解决方案就是CommonJS与和ES Modules。
我们知道CommonJS采用require导入,exports或module.exports导出。在 Commonjs 模块中,会形成一个包装函数,我们的代码将作为包装函数的执行上下文,使用的 require ,exports ,module 本质上是通过形参的方式传递到包装函数中的。在编译阶段会形成这个包装函数,在模块加载时执行这个包装函数。
当模块有相互依赖关系时,我们来看一下CommonJS是如何处理的
main.js
//main1
console.log("main导入a模块");
const a = require('./a');
//-----------------------------
//main2
console.log("main输出a模块----");
console.log(a)
a.js
//a1
console.log('a模块--导入b之前')
const b = require('./b');
//----------------------------
//a2
console.log("a输出B模块----");
console.log(b)
console.log('a模块--导入b之后')
module.exports = '我是A模块'
b.js
//b1
console.log('b模块--导入a之前')
const a = require('./a');
//b2
//----------------------------
console.log("B输出a模块----");
console.log(a)
console.log('b模块--导入a之后')
module.exports = '我是B模块'
运行main.js,输出如下:
模块a、b会被require分为上下两部分,在main.js中遇到require后,加载并执行a模块,a.js中遇到require,加载并执行b模块,b.js中又遇到require(‘./a’)发生了循环引用,因为模块a已经被加载过,直接从缓存中取,由于取出的a模块还没执行完,因此输出a是个空对象。b.js执行完后会返回到刚刚a.js的地方继续执行。三个文件的执行流程为:main1->a1->b1->b2->a2->main2。因此说CommonJS 模块在执行阶段分析模块依赖,采用深度优先遍历(depth-first traversal),执行顺序是父 -> 子 -> 父。由于存在一个缓存机制,所以可以解决循环引用的问题。
在 Es Module 中用 export 用来导出模块,import 用来导入模块。CommonJS 模块同步加载并执行模块文件,ES6 模块提前加载并执行模块文件,ES6 模块在预处理阶段分析模块依赖,在执行阶段执行模块,两个阶段都采用深度优先遍历,执行顺序是子 -> 父。
CommonJS 输出的是一个值的拷贝;ES Modules 生成一个引用,等到真的需要用到时,再到模块里面去取值,模块里面的变量,绑定其所在的模块。
CommonJS :
count.js
let num = 1
function add(){
return num++
}
module.exports = {num,add}
index.js
const {num,add} = require('./count')
console.log(num) //1
add()
console.log(num) //1
module.exports = {num,add}相当于module.exports = {num:num,add:add},num为基本数据类型,内存地址指向n1,当赋值给module.exports[‘num’] 时,内存地址已指向n2,之后对module.exports[‘num’] 的任何操作已与内存地址指向n1的num无关。
add函数为引用数据类型, 当add赋值给 module.exports[‘add’] 时,只进行了指针的复制,其内存地址依旧指向同一块数据。
通过webpack打包后的js文件也能看出两者的区别,下面先看一下CommonJS打包后的文件:
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./src/count.js":
/***/ ((module) => {
eval("let num = 1\nfunction add(count){\n return count++\n}\nmodule.exports = {num,add}\n\n//# sourceURL=webpack:///./src/count.js?");
/***/ }),
/***/ "./src/index.js":
/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
eval("const {num,add} = __webpack_require__(/*! ./count */ \"./src/count.js\")\nconsole.log(num)\nadd(num)\nconsole.log(num)\n\n\n//# sourceURL=webpack:///./src/index.js?");
/***/ })
/******/ });
/******/ // 缓存
/******/ var __webpack_module_cache__ = {};
/******/ // webpack封装的require函数
/******/ function __webpack_require__(moduleId) {
/******/ // 先找缓存中有没有该模块
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // 新建一个导出对象
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ exports: {}
/******/ };
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/ return module.exports;
/******/ }
/******/ var __webpack_exports__ = __webpack_require__("./src/index.js");
/******/ })()
;
ES Modules:
count.js
let num = 1
function add(count){
return count++
}
export {num,add}
index.js
import {num,add} from './count'
console.log(num) //1
add(num)
console.log(num) //2
在webpack打包后的js文件中可以发现,ES Modules是将值作为箭头函数的返回值,再把箭头函数赋值给导出对象,重点关注__webpack_require__.d方法。
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ "./src/count.js":
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"add\": () => (/* binding */ add),\n/* harmony export */ \"num\": () => (/* binding */ num)\n/* harmony export */ });\nlet num = 1\nfunction add(count){\n return count++\n}\n\n\n//# sourceURL=webpack:///./src/count.js?");
/***/ }),
/***/ "./src/index.js":
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _count__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./count */ \"./src/count.js\");\n\nconsole.log(_count__WEBPACK_IMPORTED_MODULE_0__.num)\n;(0,_count__WEBPACK_IMPORTED_MODULE_0__.add)(_count__WEBPACK_IMPORTED_MODULE_0__.num)\nconsole.log(_count__WEBPACK_IMPORTED_MODULE_0__.num)\n\n\n//# sourceURL=webpack:///./src/index.js?");
/******/ var __webpack_module_cache__ = {};
/******/ // 同上
/******/ function __webpack_require__(moduleId) {}
/******/ //__webpack_require__.d方法给exports添加导出变量,使用exports.key时会调用getter方法
/******/ (() => {
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ //__webpack_require__.o方法判断prop是否已经挂载到obj上
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ //__webpack_require__.r方法为当前打包的模块做个标记,表明这是个符合ES Modules规范导出的模块
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/ var __webpack_exports__ = __webpack_require__("./src/index.js");
/******/ })()
;