CommonJs和ES6 Module

209 阅读6分钟

前提摘要

在学习webpack打包的过程中,必然会面临模块的概念。这里就深入模块,帮助我们了解Webpack如何对模块进行打包合并

使用方法比较

导出
CommonJs

正确使用:

  • module.exports = {}

  • exports.add = function XX() {}

错误使用:

  • exports = {}

CommonJS模块内部会有一个module对象,用于存放当前模块的信息。CommonJS的每个模块头部都默认添加了以下代码:

var module = {
    exports: {}
}
var exports = module.exports;
  • exports.add = function XXX() {}相当于在module.exports对象上添加了属性。

  • 不可以直接给exports赋值exports = {}会使exports指向一个新的对象

  • 混用时常常使用不恰当的地方

exports.add = function() { console.info("add") };
module.exports = { name: "han meimei" }

如上的方法,moudle.exports的赋值,会导致第一次的exports.add方法被覆盖

  • 导出不一定在模块末尾,导出后面的代码依旧会执行
module.export = { name: "li lei" }
console.info("你能看到我吗?");  // 这里是可以正常执行打印的
  • module对象中有一个loaded属性用来记录该模块是否被加载过。默认是false,表示没有加载过
ES6 Module
  • 命名导出
// 写法1
export const name = "calculator";
// 写法2
const name = "calculator";
export { name };
  • 默认导出(一个模块只能有一个)
export default {
    name: "calculator"
}

我们可以将export default理解为对外输出了一个名为defalut的变量,因此不需要进行变量声明

export default "string"
export default class {}
export default function() {}
导入
CommonJs
  • 使用require方法进行模块导入
  1. require的模块是第一次被加载,这时会首先执行该模块,然后导出内容
  2. require的模块被加载过。这时该模块的代码不会再次执行,而是直接导出上次执行后得到的结果
// calculator.js
console.info("running calculator.js"); // 只会执行一次
module.exports = {
    name: "calculator",
    add: function(a, b) { return a + b }
}

// index.js
const add = require("./calculator.js").add;
const sum = add(2,3);
console.info("sum:", sum);
const moduleName = require("./calculator.js").name;
console.info("end");

// 输出结果
running calculator.js
sum: 5
end
  • 有时不需要获取导出的内容,只想要执行导入的文件。直接使用require就行
require("./task.js");
  • require接受模块动态加载
const moduleName = ["foo.js", "bar.js"];
moduleName.forEach(name => {
    require('./' + name);
})
ES6 Module
  • import语法导入
// calculator.js
const name = "calculator";
export { name };

// index.js
import { name } from "./calculator.js";

导入变量的效果相当于在当前作用域下声明了这些变量(name),并且不可以更改。就是所有导入的变量是只读的。

  • 使用import * as <myModule>可以把所有导入的变量作为属性值添加到<myModule>对象中
import * as calculator from "./calculator.js";
cosnole.info(calculator.name);
  • 复合写法
export { name } from "./calculator.js";

只支持通过命名导出的方法暴露出来的变量

区别

动态与静态
  • 动态:模块依赖关系是建立发生在代码运行阶段
  • 静态:模块依赖关系是建立在代码编译阶段
CommonJSES6 Module
动态(运行阶段)静态(编译阶段)
函数式(require)声明式(import * from "")
任意位置顶层作用域(文件顶层)
值拷贝与动态映射
CommonJSES6 Module
值拷贝动态映射
引入的值可修改引入的值不可修改

CommonJS


// calculator.js
var count = 0;
module.exports = {
    count: count,
    add: function (a, b) {
        count++;
        return a + b;
    }
}

// index.js
const count = require("calculator.js").count; // 这里的count是index文件内部自己的count
const add = require("calculator.js").add;
console.info(count);
add(1, 4);
console.info(count); // 0:calcultor.js中变量的改变不会对这里的“拷贝值”有影响,所以还是0
count++;
console.info(count);

// 输出结果
0
0
1

ES6 Module

// calculator.js
var count = 0;
export {
    count: count,
    add: function(a, b) {
        count++;
        return a + b;
    }
}

// index.js
import { count, add } from "calculator.js"; // 这里的count是映射值,理解为calculator.js中count的地址引用
console.info(count);
add(2, 3);
console.info(count); // 执行add方法后,calculator.js中的count变成了1,这里是映射值,也会是1

// 输出结果
0 
1

映射关系就像一面镜子,从镜子里我们可以实时观察到原有的事物

循环依赖

循环依赖就是指模块A依赖于模块B,同时模块B依赖于模块A 先看下两种模式的结果

  • CommonJS
// foo.js
const bar = require("./bar.js");
console.info("val of bar", bar);
module.exports = "this is foo.js";

// bar.js
const foo = require("./foo.js");
console.info("val of foo", foo);
module.exports = "this is bar.js";

// index.js
require("./foo.js");

// 打印结果
val of foo {}
val of bar this is bar.js

执行顺序分析:

  1. 执行index.js,引入foo.js文件,开始执行foo.js文件(执行权交给foo.js)
  2. 在foo.js文件中,引入bar.js文件,开始执行bar.js文件(执行权交给bar.js)
  3. 在bar.js文件中,引入foo.js文件,这里产生了依赖,但是这里的执行权不会交给foo.js,这里foo.js文件还没有执行完成(或者说没有初始化完成),这里bar.js引进的是foo.js的默认导出,也就是(module.exports = {}),这里foo的值也就是{};所以打印的是 val of foo {}
  4. bar.js文件执行完毕,并且导出module.exports = "this is bar.js";执行权重新回到foo.js
  5. foo.js此时拿到的bar.js文件导出,打印 val of bar this is bar.js

我们再从Webpack实现角度来看,从上面例子打包后,bundle中这样一段代码非常重要:

// The require function 
function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
        return installedModules[moduleId].exports;
    }
    
    // Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
        i: moduleId, // id
        l: false, // loaded 是否引入过
        exports: {}, // 默认export
    }  
}

当index.js引入了foo.js后,相当于执行了__webpack_require__函数,初始化了一个module对象并放入installedModules中。当bar.js再次引用foo.js时,又执行了__webpack_require__函数。但这次是直接从installedModules中取值,此时它的module.exports是{}。可以帮助理解和解释上面第三步出现的现象

  • ES6 Module
// foo.js
import bar from "bar.js";
console.info("val of bar", bar);
export default "this is foo.js";

// bar.js
import foo from "foo.js";
console.info("val of foo", foo);
export default "this is bar";

// index.js
import foo from "./foo.js"

// 输出顺序
val of foo undefined
val of bar this is bar

这里和CommonJS一样,没有输出正确的值。只不过和CommonJS默认导出{}不同,这里获取到的是undefined

但是在前面我们谈论过,CommonJS是值拷贝,ES6 Module的值是动态映射关系。我们就可以利用映射关系支持循环依赖

// foo.js
import bar from "./bar.js";
function foo(invoker) {
    console.info(invoker + "invokes foo.js");
    bar("foo.js");
}
export default foo;

// bar.js
import foo from "foo.js";
function bar(invoker){
    console.info(invoker + "invokes bar.js");
    foo("bar.js");
}

// index.js
import foo from "foo.js";
foo("index.js");

// 输出顺序
index.js invokes foo.js
foo.js invokes bar.js
bar.js invokes foo.js

这样的结果看着都赏心悦目

  1. 首先index.js文件开始引入foo.js文件,进入foo.js文件执行(执行权交给foo.js)
  2. foo.js文件引入bar.js文件(执行权交给bar.js)
  3. bar.js文件引入了foo.js文件,但是执行权不会交给foo.js,一直执行到结束,完成了bar函数的定义和导出,注意这里头部引入的foo.js文件还没有执行完成,foo是undefined
  4. 执行权回到foo.js,继续执行到结束,完成foo函数定义。那么此时因为ES6 Module的动态映射特性,foo已经不再是undefined,而是foo函数了。
  5. foo.js执行完成后,执行权回到index.js文件,调用foo函数,此时依次执行foo -> bar -> foo

其他

作用域
scriptsCommonJSES6 Module
全局作用域,容易污染全局环境模块本身的作用域(所有变量和函数只能自身访问)模块自身作用域