Better

Ethan的博客,欢迎访问交流

ES6 Module与CommonJS

随着 ES6 的到来,之前社区为解决 JS 模块化问题,提出的 AMD,CMD 和 UMD 等规范和实践都将渐渐退居历史舞台,在 ES6 中,import、export 取代了 require、module.exports 用来引入和导出模块,目前市场主要是ES6 Module 和 CommonJS,一个主要作用于浏览器,一个作用于Node服务端,今天我们来讨论一下两者。

输出值

看看CommonJS的例子

// a.js
var b = require('b');
console.log(b.foo);
setTimeout(() => {
  console.log(b.foo);
  console.log(require('./b').foo);
}, 1000);

// b.js
let foo = 1;
setTimeout(() => {
  foo = 2;
}, 500);
module.exports = {
  foo: foo,
};
// 1
// 1
// 1

其实这个从实现原理而言很好理解,根据JS针对原始型会直接拷贝值,所以内部改变将不会影响外部输出了。如果我们需要同步改变的话,那要如何操作呢?

  1. 在写模块的时候每次去更新module.exports上的值
  2. 暴露的 module.exports 的属性是个对象

说这个主要是想说ES6 Module的不同之处

// a.js
import { foo } from './b';
console.log(foo);
setTimeout(() => {
  console.log(foo);
  import('./b').then(({ foo }) => {
    console.log(foo);
  });
}, 1000);

// b.js
export let foo = 1;
setTimeout(() => {
  foo = 2;
}, 500);

// 1
// 2
// 2

这就是ES6 Module的不同之处,同样是针对原始型,模块内部值改变影响了模块外部值的输出,这是为什么呢?这就是ES6 Module的导出原理有关了。我们可以使用babel将语法转换一下,顺便看看ES6支持的default又是怎么处理的,我们简化代码如下:

// a.js
import bar,{foo} from './bb';
console.log(foo);
console.log(bar)
// a.js - babel
var _bb = require('./bb');

var _bb2 = _interopRequireDefault(_bb);

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

console.log(_bb.foo);
console.log(_bb2.default);

// b.js
export let foo = 1;
export default bar = 2
// b.js - babel
Object.defineProperty(exports, "__esModule", {
  value: true
});
var foo = exports.foo = 1;
setTimeout(function () {
  exports.foo = foo = 2;
}, 500);
exports.default = bar = 2;

惊讶的发现,babel转换ES6代码后,和CommonJS竟有些相似,有exports也有require。而且发现ES6同步的原理,其实和我们想使CommonJS同步的方法是一致的!

default的实现也比较简单,直接在exports上追加了default属性而已。

编译

ES6 模块编译时执行会导致有以下两个特点:

  • import 命令会被 JavaScript 引擎静态分析,优先于模块内的其他内容执行。
    • 很好理解,ES6 模版编译时,会将 import 均前置到模块首部
  • export 命令会有变量声明提前的效果。
    • export 声明的变量也是优于模块其它内容的执行的,但是具体对变量赋值需要等到执行到相应代码的时候。

动态import

ES6 模块在编译时就会静态分析,优先于模块内的其他内容执行,所以导致了我们无法写出像下面这样的代码:

if(some condition) {
  import a from './a';
}else {
  import b from './b';
}

因为编译时静态分析,导致了我们无法在条件语句或者拼接字符串模块,因为这些都是需要在运行时才能确定的结果在 ES6 模块是不被允许的,所以 动态引入 import() 应运而生。

import() 允许你在运行时动态地引入 ES6 模块,想到这,你可能也想起了 require.ensure 这个语法,但是它们的用途却截然不同的。

  • require.ensure 的出现是 webpack 的产物,它是因为浏览器需要一种异步的机制可以用来异步加载模块,从而减少初始的加载文件的体积,所以如果在服务端的话 require.ensure 就无用武之地了,因为服务端不存在异步加载模块的情况,模块同步进行加载就可以满足使用场景了。 CommonJS 模块可以在运行时确认模块加载。
  • import() 则不同,它主要是为了解决 ES6 模块无法在运行时确定模块的引用关系,所以需要引入 import()

我们先来看下它的用法:

  • 动态的 import() 提供一个基于 Promise 的 API
  • 动态的 import() 可以在脚本的任何地方使用
  • import() 接受字符串文字,你可以根据你的需要构造说明符

如果在浏览器端的 import() 的用途就会变得更广泛,比如 按需异步加载模块,那么就和 require.ensure 功能类似了。

因为是基于 Promise 的,所以如果你想要同时加载多个模块的话,可以是 Promise.all 进行并行异步加载。

动态 import() 为我们提供了以异步方式使用 ES 模块的额外功能。 根据我们的需求动态或有条件地加载它们,这使我们能够更快,更好地创建更多优势应用程序。



留言