简介

文章中就不具体解释什么是 “CommonJs” 与 “EsModule” 了,我们先简单的描述一下 “CommonJs” 与 “EsModule” 的区别。

它们有三个重大差异。

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

因为在 Webpack 中是支持这两种模块加载方式的,所以接下来我们就通过 Webpack 并结合 Demo 来具体解释一下这三个差异。


准备工作

初始化

我们先创建一个 module-demo 目录,然后执行 npm 的初始化:

mkdir module-demo && cd module-demo && npm init -y
安装 Webpack

module-demo 目录下执行以下命令:

npm install webpack webpack-cli --registry=https://registry.npm.taobao.org
配置 Webpack

module-demo 项目根目录下创建一个 webpack.config.js 文件:

touch webpack.config.js

然后将以下内容写入到 module-demo/webpack.config.js 文件:

module.exports = {
    mode: "none", // 为了测试方便,直接改成 node 模式
    output: {
        pathinfo: true, // 添加 module 的 path 信息
    },
    devtool: false, // 去掉 source-map 信息
}


创建入口文件

module-demo 项目根目录下创建一个 src 目录:

mkdir src

并在 module-demo/src 目录下创建一个 index.js 文件,用来作为我们 webpack 打包的入口文件:

touch ./src/index.js
测试

我们可以试着在 module-demo 项目根目录下执行以下命令进行 webpack 打包命令测试:

npx webpack

ES Module 与CommonJS 共存 commonjs和es6的module的区别_Webpack

可以看到,在项目的 module-demo/dist 目录下生成了一个 main.js 文件,main.js 文件就是我们通过 webpack 打包出来的资源文件。

ok,到这我们的准备工作就结束啦!

差别(一)

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

CommonJS 模块输出的是值的拷贝

我们在 module-demo/src 目录下创建一个 commonjs 目录,用来存放 commonjs 模块:

mkdir ./src/commonjs

然后在 src/commonjs 目录下创建一个 lib.js 文件:

touch ./src/commonjs/lib.js

接着将以下内容写入到 src/commonjs/lib.js 文件:

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

上面代码输出内部变量counter和改写这个变量的内部方法incCounter

然后我们在src/index.js里面加载这个模块:

// main.js
var mod = require('./commonjs/lib');

console.log(mod.counter);  // 打印 counter 值
mod.incCounter();
console.log(mod.counter); // 打印 counter 值

最后我们在 module-demo 根目录下执行 webpack 打包命令:

npx webpack

等打包完成后,我们运行一下 webpack 打包过后的 dist/main.js 文件:

node ./dist/main.js

ES Module 与CommonJS 共存 commonjs和es6的module的区别_Webpack_02

可以看到,两次 console.log 打印的都是 3,我们调用了 incCounter 方法后,counter 并没有改变,这是为什么呢?

我们看一下打包过后的 dist/main.js 文件:

/******/
(() => { // webpackBootstrap
    /******/
    var __webpack_modules__ = ([
        /* 0 */,
        /* 1 */
        /*!*****************************!*\
          !*** ./src/commonjs/lib.js ***!
          \*****************************/
        /***/ ((module) => {

// lib.js
            var counter = 3;

            function incCounter() {
                counter++;
            }

            module.exports = {
                counter: counter,
                incCounter: incCounter,
            };

            /***/
        })
        /******/]);
    /************************************************************************/
    /******/ 	// The module cache
    /******/
    var __webpack_module_cache__ = {};
    /******/
    /******/ 	// The require function
    /******/
    function __webpack_require__(moduleId) {
        /******/ 		// Check if module is in cache
        /******/
        var cachedModule = __webpack_module_cache__[moduleId];
        /******/
        if (cachedModule !== undefined) {
            /******/
            return cachedModule.exports;
            /******/
        }
        /******/ 		// Create a new module (and put it into the cache)
        /******/
        var module = __webpack_module_cache__[moduleId] = {
            /******/ 			// no module.id needed
            /******/ 			// no module.loaded needed
            /******/            exports: {}
            /******/
        };
        /******/
        /******/ 		// Execute the module function
        /******/
        __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
        /******/
        /******/ 		// Return the exports of the module
        /******/
        return module.exports;
        /******/
    }

    /******/
    /************************************************************************/
    var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
    (() => {
        /*!**********************!*\
          !*** ./src/index.js ***!
          \**********************/
// main.js
        var mod = __webpack_require__(/*! ./commonjs/lib */ 1);

        console.log(mod.counter);  // 打印 counter 值
        mod.incCounter();
        console.log(mod.counter); // 打印 counter 值
    })();

    /******/
})()
;

当我们执行 node ./dist/main.js 命令的时候,最先执行了这段代码:

(() => {
        /*!**********************!*\
          !*** ./src/index.js ***!
          \**********************/
// main.js
        var mod = __webpack_require__(/*! ./commonjs/lib */ 1);

        console.log(mod.counter);  // 打印 counter 值
        mod.incCounter();
        console.log(mod.counter); // 打印 counter 值
    })();

也就是我们的入口文件 src/index.js 文件:

// index.js
var mod = require('./commonjs/lib');

console.log(mod.counter);  // 打印 counter 值
mod.incCounter();
console.log(mod.counter); // 打印 counter 值

当我们用 require 导入 ./commonjs/lib 模块的时候,webpack 最后会编译成这样:

// index.js
var mod = require('./commonjs/lib'); // 编译前
// dist/main.js
var mod = __webpack_require__(/*! ./commonjs/lib */ 1); // 编译后

ok,我们看一下dist/main.js 文件中的 __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;
    }

可以看到,在 __webpack_require__ 方法中,首先是去缓存中找这个模块,如果缓存中没有该模块的话,就会新创建一个模块,然后执行这个模块,最后导出这个模块。

我们重点看一下执行该模块的代码:

// 执行该模块
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

__webpack_modules__ 其实就是一个模块数组,用来存放我们源文件中的所有模块信息:

var __webpack_modules__ = ([
        /* 0 */,
        /* 1 */
        /*!*****************************!*\
          !*** ./src/commonjs/lib.js ***!
          \*****************************/
        /***/ ((module) => {

// lib.js
            var counter = 3;

            function incCounter() {
                counter++;
            }

            module.exports = {
                counter: counter,
                incCounter: incCounter,
            };

            /***/
        })
        /******/]);

可以看到,我们当前 demo 中有两个模块 src/index.jssrc/commonjs/lib.js,因为 src/index.js 没有导出任何模块信息,所以 __webpack_modules__ 数组的第一个子项为 undefined,当我们执行:

__webpack_modules__[moduleId](module, module.exports, __webpack_require__);

代码的时候,我们传递进来的是 1

// dist/main.js
var mod = __webpack_require__(/*! ./commonjs/lib */ 1); // 编译后

所以最后会执行 __webpack_modules__ 数组的第 1 项内容,也就是我们的 src/commonjs/lib.js 模块:

var __webpack_modules__ = ([
        /* 0 */,
        /* 1 */
        /*!*****************************!*\
          !*** ./src/commonjs/lib.js ***!
          \*****************************/
        /***/ ((module) => {

// lib.js
            var counter = 3;

            function incCounter() {
                counter++;
            }

            module.exports = {
                counter: counter,
                incCounter: incCounter,
            };

            /***/
        })
        /******/]);

可以看到,src/commonjs/lib.js 被编译成了这样:

((module) => {
						// lib.js
            var counter = 3;

            function incCounter() {
                counter++;
            }

            module.exports = {
                counter: counter,
                incCounter: incCounter,
            };
        })

所以我们在 src/index.js 中引入的 ./commonjs/lib 模块最后获取的是 module.exports 对象:

{
                counter: counter,
                incCounter: incCounter,
            }

直接把 var counter = 3; 的值给了 module.exports 对象,所以即使我们后面又调用了 incCounter 方法,但是改变的是 var counter = 3; 这个变量的值,而我们的 module.exports 对象的 counter 值没有改变,所以最后两次输出的都是 3,所以 CommonJs 中,当我们用 module.exportsexports 导出模块的时候,输出的是值的拷贝。

EsModule 模块输出的是值的引用

EsModule 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。

下面我们用 WebpackEsModule 处理方式解析一下 EsModule 的原理。

我们先在 src 目录下创建一个 esmodule 目录:

mkdir ./src/esmodule

然后在 src/esmodule 目录下也创建一个 lib.js 文件:

touch ./src/esmodule/lib.js

接着用 EsModule 的方式重写一下 src/commonjs/lib.js 文件:

// lib.js
export var counter = 3;
export function incCounter() {
    counter++;
}

最后我们改一下入口文件 src/index.js 文件:

// main.js
// var mod = require('./commonjs/lib');
import * as mod from "./esmodule/lib";
console.log(mod.counter);  // 打印 counter 值
mod.incCounter();
console.log(mod.counter); // 打印 counter 值

我们执行 webpack 打包操作:

npx webpack

同样,等打包完毕后运行 dist/main.js 文件:

node ./dist/main.js

ES Module 与CommonJS 共存 commonjs和es6的module的区别_前端_03

可以看到,第一次输出的是 3,然后执行完 mod.incCounter() 后,mod.counter 变成了 4,这是为什么呢?

我们打开 dist/main.js 源码看看:

/******/
(() => { // webpackBootstrap
    /******/
    "use strict";
    /******/
    var __webpack_modules__ = ([
        /* 0 */,
        /* 1 */
        /*!*****************************!*\
          !*** ./src/esmodule/lib.js ***!
          \*****************************/
        /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

            __webpack_require__.r(__webpack_exports__);
            /* harmony export */
            __webpack_require__.d(__webpack_exports__, {
                /* harmony export */   "counter": () => (/* binding */ counter),
                /* harmony export */   "incCounter": () => (/* binding */ incCounter)
                /* harmony export */
            });
// lib.js
            var counter = 3;

            function incCounter() {
                counter++;
            }

            /***/
        })
        /******/]);
    /************************************************************************/
    /******/ 	// The module cache
    /******/
    var __webpack_module_cache__ = {};
    /******/
    /******/ 	// The require function
    /******/
    function __webpack_require__(moduleId) {
        /******/ 		// Check if module is in cache
        /******/
        var cachedModule = __webpack_module_cache__[moduleId];
        /******/
        if (cachedModule !== undefined) {
            /******/
            return cachedModule.exports;
            /******/
        }
        /******/ 		// Create a new module (and put it into the cache)
        /******/
        var module = __webpack_module_cache__[moduleId] = {
            /******/ 			// no module.id needed
            /******/ 			// no module.loaded needed
            /******/            exports: {}
            /******/
        };
        /******/
        /******/ 		// Execute the module function
        /******/
        __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
        /******/
        /******/ 		// Return the exports of the module
        /******/
        return module.exports;
        /******/
    }

    /******/
    /************************************************************************/
    /******/ 	/* webpack/runtime/define property getters */
    /******/
    (() => {
        /******/ 		// define getter functions for harmony exports
        /******/
        __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/runtime/hasOwnProperty shorthand */
    /******/
    (() => {
        /******/
        __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
        /******/
    })();
    /******/
    /******/ 	/* webpack/runtime/make namespace object */
    /******/
    (() => {
        /******/ 		// 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__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
    (() => {
        /*!**********************!*\
          !*** ./src/index.js ***!
          \**********************/
        __webpack_require__.r(__webpack_exports__);
        /* harmony import */
        var _esmodule_lib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./esmodule/lib */ 1);
// main.js
// var mod = require('./commonjs/lib');

        console.log(_esmodule_lib__WEBPACK_IMPORTED_MODULE_0__.counter);  // 打印 counter 值
        _esmodule_lib__WEBPACK_IMPORTED_MODULE_0__.incCounter();
        console.log(_esmodule_lib__WEBPACK_IMPORTED_MODULE_0__.counter); // 打印 counter 值
    })();

    /******/
})()
;

我们先看一下入口文件 src/index.js

(() => {
        /*!**********************!*\
          !*** ./src/index.js ***!
          \**********************/
        __webpack_require__.r(__webpack_exports__);
        // 加载 esmodule/lib.js 模块
        var _esmodule_lib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
        console.log(_esmodule_lib__WEBPACK_IMPORTED_MODULE_0__.counter);  // 打印 counter 值
   			// 执行 esmodule/lib.js 模块的 incCounter 方法
        _esmodule_lib__WEBPACK_IMPORTED_MODULE_0__.incCounter();
        console.log(_esmodule_lib__WEBPACK_IMPORTED_MODULE_0__.counter); // 打印 counter 值
    })();

可以看到,首先是去用 __webpack_require__ 方法去加载了 esmodule/lib.js 模块,esmodule/lib.jsmoduleId1esmodule/lib.js 被转换成了 __webpack_modules__ 数组的第一项:

/*!*****************************!*\
          !*** ./src/esmodule/lib.js ***!
          \*****************************/
        ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
            __webpack_require__.r(__webpack_exports__);
          // 定义模块的输出
            __webpack_require__.d(__webpack_exports__, {
                "counter": () => (/* binding */ counter), // 返回 counter 的引用
                "incCounter": () => (/* binding */ incCounter) // 返回 incCounter 的引用
            });
						// lib.js
            var counter = 3;

            function incCounter() {
                counter++;
            }
        })

可以看到,这一次不再是直接导出一个 module.exports 对象了,这一次是利用 __webpack_require__.d 方法定义了模块的输出:

// 定义模块的输出
__webpack_require__.d(__webpack_exports__, {
  "counter": () => (/* binding */ counter), // 返回 counter 的引用
  "incCounter": () => (/* binding */ incCounter) // 返回 incCounter 的引用
});

ok,我们重点看一下 __webpack_require__.d 方法:

__webpack_require__.d = (exports, definition) => {
            // 遍历需要导出的属性
            for (var key in definition) {
                /******/
                if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
                    // 在导出的 exports 对象上定义需要导出的属性
                    Object.defineProperty(exports, key, {enumerable: true, get: definition[key]});
                    /******/
                }
                /******/
            }
            /******/
        };

可以看到,直接遍历了需要导出的所有属性,然后在导出的对象 exports 上面用 Object.defineProperty 方法定义了这些属性,这里的 exports 就是我们的 mod 对象:

// var mod = require('./commonjs/lib');
import * as mod from "./esmodule/lib";

所以当我们用 mod 去访问这些导出的属性的时候:

console.log(mod.counter);  // 访问 mod 的 counter 属性
mod.incCounter(); // 访问 mod 的 incCounter 属性
console.log(mod.counter);  // 访问 mod 的 counter 属性

最后都会走 exports 对象的属性描述器的 get 方法,也就是用 __webpack_require__.d 方法传入的参数:

/*!*****************************!*\
          !*** ./src/esmodule/lib.js ***!
          \*****************************/
        ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
           ...
          // 定义模块的输出
            __webpack_require__.d(__webpack_exports__, {
                "counter": () => (/* binding */ counter), // 返回 counter 的引用
                "incCounter": () => (/* binding */ incCounter) // 返回 incCounter 的引用
            });
						// lib.js
            var counter = 3;

            function incCounter() {
                counter++;
            }
        })

所以在 EsModule 中,模块输出的是值的引用,而这个引用在 webpack 中被转换成了属性描述器,属性描述器的 get 方法中返回的是这个值的内容。

差别(二)

CommonJS 模块是运行时加载

看一下我们当前 src/commonjs/lib.js 文件:

// lib.js
var counter = 3;
function incCounter() {
    counter++;
}
module.exports = {
    counter: counter,
    incCounter: incCounter,
};

最后会被 webpack 转换成:

var __webpack_modules__ = ([
        /* 0 */,
        /* 1 */
        /*!*****************************!*\
          !*** ./src/commonjs/lib.js ***!
          \*****************************/
        /***/ ((module) => {

// lib.js
            var counter = 3;

            function incCounter() {
                counter++;
            }

            module.exports = {
                counter: counter,
                incCounter: incCounter,
            };

            /***/
        })
        /******/]);

当我们在 src/index.js 中用 require 引入 commonjs/lib.js 模块的时候,因为 js 执行是同步的,所以会停止 src/index.js 的执行,进入到 commonjs/lib.js 模块,在 commonjs/lib.js 模块中,首先是走了:

var counter = 3;

定义了一个 counter 变量,然后继续走:

function incCounter() {
     counter++;
}

又声明了一个 incCounter 方法,然后继续走:

module.exports = {
                counter: counter,
                incCounter: incCounter,
            };

最后导出了一个 module.exports 对象,当 commonjs/lib.js 模块代码执行完毕后,再次回到了 src/index.js 文件中,并返回 commonjs/lib.js 模块中导出的 module.exports 对象:

var mod = require('./commonjs/lib');
console.log(mod.counter);  // 打印 counter 值
mod.incCounter();
console.log(mod.counter); // 打印 counter 值

也就是这里的 mod 变量,然后 js 继续往下执行…

为了再次验证一下,我们可以修改一下 src/commonjs/lib.js 文件:

// lib.js
module.exports = {
    counter: counter,
    incCounter: incCounter,
};
var counter = 3;
function incCounter() {
    counter++;
}

可以看到,我们把 module.exports 对象的声明放在了 lib.js 模块的第一行去执行了。

接着我们修改一下入口文件 src/index.js 文件,引入 commonjs/lib.js 模块:

// main.js
var mod = require('./commonjs/lib');
// import * as mod from "./esmodule/lib";
console.log(mod.counter);  // 打印 counter 值
mod.incCounter();
console.log(mod.counter); // 打印 counter 值

再次执行 webpack 编译:

npx webpack

等编译完毕后执行 dist/main.js 文件:

$ node ./dist/main.js 
undefined
undefined

可以看到,两次 console.log 打印的都是 undefined,这是为什么呢?

其实我们只需要看一眼 commonjs/lib.js 经过 webpack 转换过后的结果就一目了然了,我们找到 dist/main.js 中的 lib.js 模块:

/* 1 */
        /*!*****************************!*\
          !*** ./src/commonjs/lib.js ***!
          \*****************************/
        /***/ ((module) => {

					// lib.js
          module.exports = {
                counter: counter,
                incCounter: incCounter,
            };
            var counter = 3;

            function incCounter() {
                counter++;
            }

            /***/
        })

当我们在 src/index.js 中导入 lib.js 模块的时候,最先走的是:

module.exports = {
                counter: counter,
                incCounter: incCounter,
            };

而这个时候 counter 还没有被赋值,所以返回的 module.exports 对象中的 counter 属性是 undefined,那么有小伙伴要说了,为什么 incCounter 可以访问呢?因为在 js 中,函数是有提升效果的,即使我们在后面声明,我们也可以在前面访问的,变量在 js 中也会提升,但是仅仅是声明的提升,赋值操作不会被提升的。

EsModule 模块是编译时输出接口

看一下 src/esmodule/lib.js 文件:

// lib.js
export var counter = 3;
export function incCounter() {
    counter++;
}

最后会被 webpack 转换成:

/* 1 */
        /*!*****************************!*\
          !*** ./src/esmodule/lib.js ***!
          \*****************************/
        /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

            __webpack_require__.r(__webpack_exports__);
            /* harmony export */
            __webpack_require__.d(__webpack_exports__, {
                /* harmony export */   "counter": () => (/* binding */ counter),
                /* harmony export */   "incCounter": () => (/* binding */ incCounter)
                /* harmony export */
            });
						// lib.js
            var counter = 3;

            function incCounter() {
                counter++;
            }
            /***/
        })

当我们在 src/index.js 引入 esmodule/lib.js 模块的时候,首先走了:

/* harmony export */
__webpack_require__.d(__webpack_exports__, {
  /* harmony export */   "counter": () => (/* binding */ counter),
  /* harmony export */   "incCounter": () => (/* binding */ incCounter)
  /* harmony export */
});

导出了这个模块的内容,内容为这个模块值的引用。

接着走了:

var counter = 3;

声明了一个 counter 变量。

接着走了:

function incCounter() {
   counter++;
}

声明了一个 incCounter 方法。

最后 esmodule/lib.js 执行完毕后回到 src/index.js 文件:

import * as mod from "./esmodule/lib";

这里的 mod 返回的就是 /esmodule/lib.js 模块输出值的引用。

因为 EsModule 是编译时输出接口,所以我们不能进行这样的操作:

// lib.js
if(true){
    export var counter = 3;
    export function incCounter() {
        counter++;
    }
}

这样 webpack 编译的时候就会报错了:

ES Module 与CommonJS 共存 commonjs和es6的module的区别_es6_04


说 “import 和 export 只能出现在顶层”,这是为什么呢?因为在编译的时候,首先会去找 exportimport 声明,然后会把 exportimport 提升到内容的头部,我们可以验证一下。

我们修改一下 src/index.js 文件:

console.log(mod.counter);  // 打印 counter 值
mod.incCounter();
console.log(mod.counter); // 打印 counter 值
import * as mod from "./esmodule/lib";

可以看到,我把 ./esmodule/lib 的导入放在了代码的最后一行,我们编译运行:

192:module-demo yinqingyang$ npx webpack
asset main.js 3.53 KiB [emitted] (name: main)
runtime modules 670 bytes 3 modules
cacheable modules 286 bytes
  ./src/index.js 205 bytes [built] [code generated]
  ./src/esmodule/lib.js 81 bytes [built] [code generated]
webpack 5.25.0 compiled successfully in 167 ms
192:module-demo yinqingyang$ node ./dist/main.js 
3
4

可以看到,照样可以运行,这是为什么呢?

我们看一下编译过后的 src/index.js 文件:

var _esmodule_lib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./esmodule/lib */ 1);
console.log(_esmodule_lib__WEBPACK_IMPORTED_MODULE_0__.counter);  // 打印 counter 值
_esmodule_lib__WEBPACK_IMPORTED_MODULE_0__.incCounter();
console.log(_esmodule_lib__WEBPACK_IMPORTED_MODULE_0__.counter); // 打印 counter 值

可以看到,在编译的时候,会自动的把我们声明的 import 提到代码的最前面。

export 也是一样,比如我们修改一下 src/esmodule/lib.js 文件:

// lib.js
function incCounter() {
    counter++;
}
export {
    counter,
    incCounter
}
var counter = 3;

经过 webpack 编译后会变成这样:

__webpack_require__.d(__webpack_exports__, {
                /* harmony export */   "counter": () => (/* binding */ counter),
                /* harmony export */   "incCounter": () => (/* binding */ incCounter)
                /* harmony export */
            });
						// lib.js
            function incCounter() {
                counter++;
            }
            var counter = 3;

可以看到,不管我们把 export 放在哪,最后编译过后都会放在内容的头部。

当文件中 export 跟 import 同时存在时,export 在 import 前面,比如我们修改一下 src/index.js 文件,让它也导出一个属性:

// main.js
// var mod = require('./commonjs/lib');
console.log(mod.counter);  // 打印 counter 值
mod.incCounter();
console.log(mod.counter); // 打印 counter 值
import * as mod from "./esmodule/lib";
export default mod;

最后会被编译成:

// export default mod;
__webpack_require__.d(__webpack_exports__, {
  "default": () => (__WEBPACK_DEFAULT_EXPORT__)
})
// import * as mod from "./esmodule/lib";
var _esmodule_lib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
console.log(_esmodule_lib__WEBPACK_IMPORTED_MODULE_0__.counter);  // 打印 counter 值
_esmodule_lib__WEBPACK_IMPORTED_MODULE_0__.incCounter();
console.log(_esmodule_lib__WEBPACK_IMPORTED_MODULE_0__.counter); // 打印 counter 值

const __WEBPACK_DEFAULT_EXPORT__ = (_esmodule_lib__WEBPACK_IMPORTED_MODULE_0__);

差别(三)

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

其实解析完差别一跟二后,我觉得都不用解析三了,小伙伴自己把前面两个差别结合编译过后的结果走一遍就清楚了,这里就留给小伙伴自己去感受了哈。

扩展

循环加载

“循环加载”(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。

// a.js
var b = require('b');

// b.js
var a = require('a');

通常,“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。

但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现a依赖bb依赖cc又依赖a这样的情况。这意味着,模块加载机制必须考虑“循环加载”的情况。

对于 JavaScript 语言来说,目前最常见的两种模块格式 CommonJS 和 ES6,处理“循环加载”的方法是不一样的,返回的结果也不一样。

CommonJS 模块的循环加载

CommonJS 模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

我们以 Node 官方文档 里面的例子分析一下。

我们去 src/commonjs目录下创建一个 a.js 文件, a.js代码:

exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');

我们去 src/commonjs目录下创建一个 b.js 文件, b.js代码:

exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');

然后我们修改一下 src/index.js 文件:

require("./commonjs/a");

重新编译运行:

npx webpack && node ./dist/main.js

结果:

在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕

下面我们结合 webpack 编译过后的代码,一步一步分析一下为什么会有这样的结果。

前面我们知道,CommonJs 是运行时加载,并且时同步的操作,所以当我们 src/index.js 加载 a.js 模块的时候:

require("./commonjs/a");

代码会走到 a.js 模块, a.js 模块编译后的代码为:

/* 1 */
        /*!***************************!*\
          !*** ./src/commonjs/a.js ***!
          \***************************/
        /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
            exports.done = false;
            var b = __webpack_require__(/*! ./b.js */ 2);
            console.log('在 a.js 之中,b.done = %j', b.done);
            exports.done = true;
            console.log('a.js 执行完毕');
            /***/
        }),

首先走了:

exports.done = false;

这个时候 a.js 模块的 exports 对象的 done 属性已经被赋值为了 false,继续往下走:

var b = __webpack_require__(/*! ./b.js */ 2);

这个时候就会去加载 b.js 模块,b.js 模块编译过后为:

/* 2 */
        /*!***************************!*\
          !*** ./src/commonjs/b.js ***!
          \***************************/
        /***/ ((__unused_webpack_module, exports, __webpack_require__) => {

            exports.done = false;
            var a = __webpack_require__(/*! ./a.js */ 1);
            console.log('在 b.js 之中,a.done = %j', a.done);
            exports.done = true;
            console.log('b.js 执行完毕');

            /***/
        })

首先执行 b.js 模块的这一行代码:

exports.done = false;

这个时候 b.js 模块的 exports 对象的 done 属性已经被赋值为了 false,继续往下走:

var a = __webpack_require__(/*! ./a.js */ 1);

由于 a.js 模块已经被执行了,所以直接从缓存中获取到 a.js 模块 exports 对象,b.js 模块继续往下执行:

console.log('在 b.js 之中,a.done = %j', a.done);

目前 a.js 模块的 exports 对象中只有一个 done=false 属性,所以直接输出了:

在 b.js 之中,a.done = false

b.js 模块继续往下执行:

exports.done = true;

b.js 模块的输出对象 exports 上的 done 被赋值 trueb.js 模块继续往下执行:

console.log('b.js 执行完毕');

打印:

b.js 执行完毕

程序这个时候跳出 b.js 模块代码,回到 a.js 模块代码:

console.log('在 a.js 之中,b.done = %j', b.done);

因为 b.js 模块执行完毕了,而且 b.js 模块的输出对象 exports 上的 done 被赋值 true,所以直接打印:

在 a.js 之中,b.done = true

a.js 模块继续执行:

exports.done = true;

这个时候 a.js 模块的 exports 对象的 done 属性已经被赋值为了 true,继续往下走:

console.log('a.js 执行完毕');

打印:

a.js 执行完毕

整个流程结束。

总之,CommonJS 输入的是被输出值的拷贝,不是引用。

另外,由于 CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。

EsModule 模块的循环加载

同样是上面的 a.jsb.js 模块,我们用 EsModule 的方式实现一下。

首先在 src/esmodule 目录下面创建一个 a.js 文件:

export var done = false;
import * as b from './b.js';
console.log('在 a.js 之中,b.done = %j', b.done);
done = true;
console.log('a.js 执行完毕');

然后在在 src/esmodule 目录下面创建一个 b.js 文件:

export var done = false;
import * as a from './a.js';
console.log('在 b.js 之中,a.done = %j', a.done);
done = true;
console.log('b.js 执行完毕');

最后修改一下 src/index.js 入口文件:

// require("./commonjs/a");
import "./esmodule/a";

重新编译运行:

npx webpack && node ./dist/main.js

结果:

在 b.js 之中,a.done = undefined
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕

下面我们结合 webpack 编译过后的代码,一步一步分析一下为什么会有这样的结果。

前面我们知道,EsModule 是编译时输出,并且是异步操作,所以当我们 src/index.js 加载 a.js 模块的时候:

import "./esmodule/a";

代码会走到 a.js 模块, a.js 模块编译后的代码为:

/* 1 */
        /*!***************************!*\
          !*** ./src/esmodule/a.js ***!
          \***************************/
        /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

            __webpack_require__.r(__webpack_exports__);
            /* harmony export */
            __webpack_require__.d(__webpack_exports__, {
                "done": () => (/* binding */ done)
            });
            /* harmony import */
            var _b_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./b.js */ 2);
            var done = false;

            console.log('在 a.js 之中,b.done = %j', _b_js__WEBPACK_IMPORTED_MODULE_0__.done);
            done = true;
            console.log('a.js 执行完毕');
            /***/
        })

首先是执行 a.js 模块这段代码:

__webpack_require__.d(__webpack_exports__, {
                "done": () => (/* binding */ done)
            });

a.js 模块的输出对象 exports 上定义了一个 done 属性,结果指向 done 的值。

a.js 模块继续往下执行:

var _b_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./b.js */ 2);

加载了 b.js 模块,所以代码走到了 b.js 模块:

/* 2 */
        /*!***************************!*\
          !*** ./src/esmodule/b.js ***!
          \***************************/
        /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

            __webpack_require__.r(__webpack_exports__);
            /* harmony export */
            __webpack_require__.d(__webpack_exports__, {
                /* harmony export */   "done": () => (/* binding */ done)
                /* harmony export */
            });
            /* harmony import */
            var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a.js */ 1);
            var done = false;

            console.log('在 b.js 之中,a.done = %j', _a_js__WEBPACK_IMPORTED_MODULE_0__.done);
            done = true;
            console.log('b.js 执行完毕');

            /***/
        })

b.js 模块首先执行以下代码:

__webpack_require__.d(__webpack_exports__, {
                "done": () => (/* binding */ done)
    });

b.js 模块的输出对象 exports 上定义了一个 done 属性,结果指向 done 变量的值。

b.js 模块继续往下执行:

var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a.js */ 1);

引入了 a.js 模块,由于 a.js 模块已经被执行了,所以直接从缓存中获取到 a.js 模块 exports 对象:

{
       "done": () => (/* binding */ done)
}

b.js 模块继续往下执行:

var done = false;

定义了一个 done 变量,并且值为 false

b.js 模块继续往下执行:

console.log('在 b.js 之中,a.done = %j', _a_js__WEBPACK_IMPORTED_MODULE_0__.done);

获取 a.js 模块中的 done 属性:

"done": () => (/* binding */ done)

执行 a.js 模块 exports 对象的 done 属性描述器的get 方法:

() => (/* binding */ done)

返回 a.js 模块中声明的 done 变量,而这个时候的 done 变量只是由于变量的提升完成了声明,但并没有赋值,所以在 b.js 模块中直接输出:

在 b.js 之中,a.done = undefined

因为我们在 a.js 模块中声明 done 变量的时候用的是 var,如果我们用的是 let,程序就会直接报错了,因为 let 没有变量提升的效果。

b.js 模块继续往下执行:

done = true;

b.js 中的 done 变量被赋值为 true

b.js 模块继续往下执行:

console.log('b.js 执行完毕');

b.js 模块输出:

b.js 执行完毕

代码执行回到 a.js 模块:

var done = false;

a.js 模块中的 done 变量赋值 false

a.js 模块继续往下:

console.log('在 a.js 之中,b.done = %j', _b_js__WEBPACK_IMPORTED_MODULE_0__.done);

由于 b.js 模块已经执行完毕,并且最后对 done 属性赋值了 true,所以这里就直接输出了:

在 a.js 之中,b.done = true

a.js 模块继续往下:

done = true; // 给 done 变量赋值为 true
console.log('a.js 执行完毕');

输出:

a.js 执行完毕

其实在 EsModule 中,当循环引用了时候,如果属性没有提升效果的话,肯定是会报错的,提升效果说的就是 var 跟 “函数”,所以在 EsModule 中,一定要避免出现循环引用的问题。

解决循环引用
  1. 这东西没啥好方法,写代码的时候条理一定要清晰
  2. 利用一些代码检测工具

我这里以 Webpack 为例,可以安装 circular-dependency-plugin 插件去检测代码中的循环引用。

我们还是演示一下吧。

首先在 module-demo 目录下安装 circular-dependency-plugin 插件:

npm install -D circular-dependency-plugin

然后修改 webpack.config.js 文件,引入 circular-dependency-plugin 插件:

const CircularDependencyPlugin = require('circular-dependency-plugin')

module.exports = {
    mode: "none", // 为了测试方便,直接改成 node 模式
    target: "node",
    output: {
        pathinfo: true, // 添加 module 的 path 信息
    },
    devtool: false, // 去掉 source-map 信息
    plugins: [
        new CircularDependencyPlugin()
    ]
}

然后我们执行 webpack 打包:

npx webpack

ES Module 与CommonJS 共存 commonjs和es6的module的区别_Webpack_05

可以看到,打包的时候直接警告提示了。

异步模块

前面我们知道,EsModule 处理时编译时进行的,所以 importexport 只能放在顶层代码中,但是在 ES2020 中,我们可以用 import() 在运行时导入一个异步模块。

我们修改一下 src/index.js 文件:

// main.js
// var mod = require('./commonjs/lib');
setTimeout(()=>{
    import("./esmodule/lib").then((module)=>{
        console.log(module.counter);  // 打印 counter 值
        module.incCounter();
        console.log(module.counter); // 打印 counter 值
    });
},1000)

因为我们需要在 node 环境中运行,所以我们把 webpacktarget 改成 node,修改找到 webpack.config.js 文件:

module.exports = {
    mode: "none", // 为了测试方便,直接改成 node 模式
    target: "node", // 改为 node 环境
    output: {
        pathinfo: true, // 添加 module 的 path 信息
    },
    devtool: false, // 去掉 source-map 信息
}

然后我们重新编译:

npx webpack

等编译成功后,我们运行 dist/main.js 文件,你会发现,过了 1s 后打印了 console.log 的值,效果我就不演示了,小伙伴自己跑跑。

我们看一下编译过后的 dist 目录:

ES Module 与CommonJS 共存 commonjs和es6的module的区别_前端_06

可以看到,编译出了两个文件 1.jsmain.js1.js 就是我们的 esmodule/lib.js 文件:

exports.id = 1;
exports.ids = [1];
exports.modules = [
/* 0 */,
/* 1 */
/*!*****************************!*\
  !*** ./src/esmodule/lib.js ***!
  \*****************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "counter": () => (/* binding */ counter),
/* harmony export */   "incCounter": () => (/* binding */ incCounter)
/* harmony export */ });
// lib.js
function incCounter() {
    counter++;
}
var counter = 3;
/***/ })
];
;

所以当我们用 import() 去导入一个模块的时候,webpack 会把它当成一个异步模块,并返回一个 Promise 对象,异步返回这个模块的内容,webpack 中对于 node 环境异步模块的实现我就不详细分析了,小伙伴自己去看编译过后的源码哦。

参考文献