Adamwu1992 / adamwu1992.github.io

My Blog
2 stars 0 forks source link

ES_Module & CommonJS #13

Open Adamwu1992 opened 5 years ago

Adamwu1992 commented 5 years ago

ES_Module & CommonJS

模块化规范

随着打包工具的兴起,用户浏览器端的模块化方案逐渐失去了使用价值,更好的替代方案是通过webpack等工具打包成bundle再根据需要分块,而babel则允许我们提前在代码里使用ES Module,在编译阶段转化为CommonJS再交给打包工具去处理。所以接下来着重探讨CommonJSES Module的细节.

使用方法

ES Module to CommonJS

前面谈到目前还没有原生支持的ES Module实现,所以我们日常书写的代码模块都是转译到CommonJS处理的,这里用Babel来看看ES Module和CommonJS的对应关系。

babel配置详见官方文档

export const a = 100
export const b = () => a + 1
export default 99
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = exports.b = exports.a = void 0;
var a = 100;
exports.a = a;

var b = function b() {
  return a + 1;
};

exports.b = b;
var _default = 99;
exports.default = _default;

首先看导出语法的转译,在CommonJS中的全局对象定义了一个标志来表示这个是ES6的模块,然后给后面要用到的属性先进行初始化(也就是赋值为undefined),然后依次把export导出的变量挂载到exports上,而对于export default导出的值则是挂载到exports.default上。

import { a } from 'a'
import b from 'b'

console.log(a)
console.log(b)
"use strict";

var _a = require("a");

var _b = _interopRequireDefault(require("b"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

console.log(_a.a);
console.log(_b.default);

再来看导入的语法。对于import { a } from 'a',CommonJS和ES Module的处理方式是一样的,所以仅仅是变成var _a = require("a"),但是对于import b from 'b',在CommonJS中意味着要导出b模块中所有的内容到b变量下,而在ES Module中则表示把b模块中的default导出到b变量下,所以babel在此处判断了一下模块所属类型。

CommonJS vs ES Module

这两种模块规范显然是有很大区别的,上面已经涉及到一些了,这里再做一些总结:

第一点已经讲过。

第二点关于导入的属性的绑定关系,这两者有很大区别:

// base.js
let a = 100
let add = () => ++a
export { a, add }

// main.js
import { a, add } from './base'
console.log(a) // 100
add()
console.log(a) // 101
// base.js
let a = 100
let add = () => ++a

let b = { count: 0 }
let increment = () => {
  b.count += 1
  return b
}
exports = { a, add, b, increment }

// main.js
const { a, add, b, increment } = require('./base')
console.log(a) // 100
add()
console.log(a) // 100

console.log(b) // { count: 0 }
increment()
console.log(b) // { count: 1 }

在CommonJS中,当导入的是基础类型时,会复制一份到当前模块,之后原模块再改变这个变量不会引起拷贝的变化,这就是值传递;如果导入的是一个对象时,则是遵循引用传递的原则,会复制一份导入对象的地址到当前模块,如果原模块直接修改这个对象的属性,在当前模块里按照地址去读取对象,会是改变后的对象,如果原模块修改这个对象,则会重新分配一个新地址储存新对象,当前模块按照原地址读取的是未改变的元对象,这就是引用传递。

在ES Module中,不管导入的是基础类型的属性还是一个对象,当导出的对象发生变化,当前模块都能追踪到变化后的值,这就是所谓的强绑定

有关于第三点,和两种模块解析的时机有关。 CommonJS是在运行时期解析,所以可以动态导入,比如:

if (process.env.NODE_NEV === 'development') {
  new require('vConsole')()
}

而ES Module要求导入语句必须在模块的顶层,之前不能有其他代码。这是因为模块解析在静态分析阶段就发生了,静态分析器不会知道process.env.NODE_NEV === 'development'这个条件是否成立。

webpacktree shaking功能,要求我们如果用到了babel-preset-env,需要阻止将ES Module代码转化为CommonJS代码,这和它们的第三点差异有很大关系。 tree shaking也是发生在静态分析阶段的,而require语句需要等到运行时才会决定是否需要引入,webpack不能确信这个模块是不需要的,也就不能删掉这个模块。

阻止babel转译ES Module代码

{
presets: [["es2015", { modules: false }], "react", "stage-3"]
}

参考

Webpack: Import vs Require, and why