commonjs 与 esm 的区别

栏目: JavaScript · 发布时间: 6年前

内容简介:js 社区存在多种模块化规范,其中最常使用到的是 node 本身实现的 commonjs 和 es6 标准的 esm。commonjs 和 esm 存在多种根本上的区别,详细的比较在阮一峰的《es6标准入门》已经写得很详细了,这里我想用自己的思路重新总结一下。同时分析一下 babel 对于 esm 的编译转换,存在的局限。commonjs 和 esm 的主要区别可以概括成以下几点:

js 社区存在多种模块化规范,其中最常使用到的是 node 本身实现的 commonjs 和 es6 标准的 esm。

commonjs 和 esm 存在多种根本上的区别,详细的比较在阮一峰的《es6标准入门》已经写得很详细了,这里我想用自己的思路重新总结一下。同时分析一下 babel 对于 esm 的编译转换,存在的局限。

commonjs 和 esm 的主要区别可以概括成以下几点:

  1. 输出拷贝 vs 输出引用
  2. esm 的 import read-only 特性
  3. esm 存在 export/import 提升

下面对这三点做具体分析。

输出拷贝 vs 输出引用

首先看个 commonjs 输出拷贝的例子:

// a.js
let a = 1;
let b = { num: 1 }
setTimeout(() => {
    a = 2;
    b = { num: 2 };
}, 200);
module.exports = {
    a,
    b,
};

// main.js
// node main.js
let {a, b} = require('./a');
console.log(a);  // 1
console.log(b);  // { num: 1 }
setTimeout(() => {
    console.log(a);  // 1
    console.log(b);  // { num: 1 }
}, 500);
复制代码

所谓输出拷贝,如果了解过 node 或者 webpack 对 commonjs 的实现(不了解可以看我之前的文章),就会知道:exports 对象是模块内外的唯一关联, commonjs 输出的内容,就是 exports 对象的属性,模块运行结束,属性就确定了。

再看 esm 输出引用的例子:

// a.mjs
let a = 1;
let b = { num: 1 }
setTimeout(() => {
    a = 2;
    b = { num: 2 };
}, 200);
export {
    a,
    b,
};

// main.mjs
// node --experimental-modules main.mjs
import {a, b} from './a';
console.log(a);  // 1
console.log(b);  // { num: 1 }
setTimeout(() => {
    console.log(a);  // 2
    console.log(b);  // { num: 2 }
}, 500);
复制代码

这就是 esm 输出引用跟 commonjs 输出值的区别,模块内部引用的变化,会反应在外部,这是 esm 的规范。

esm 的 import read-only 特性

read-only 的特性很好理解,import 的属性是只读的,不能赋值,类似于 const 的特性,这里就不举例解释了。

esm 存在 export/import 提升

esm 对于 import/export 存在提升的特性,具体表现是规范规定 import/export 必须位于模块顶级,不能位于作用域内;其次对于模块内的 import/export 会提升到模块顶部,这是在编译阶段完成的。

esm 的 import/export 提升在正常情况下,使用起来跟 commonjs 没有区别,因为一般情况下,我们在引入模块的时候,都会在模块的同步代码执行完才获取到输出值。所以即使存在提升,也无法感知。

所以要想验证这个事实,需要考虑到循环依赖的情况。循环依赖指的是模块A依赖模块B,模块B又依赖模块A,互相依赖产生了死循环。所以各个模块方案本身设计了一套规则来解决这个问题。在循环依赖的情况下,模块会出现执行中断,然后我们可以看到 import/export 提升和 commonjs 的区别。

这里用2个循环依赖的例子来解释,首先看 commonjs 的表现:

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

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

// main.js
let a = require('./a');
let b = require('./b');
console.log('main.js: a.done = %j, b.done = %j', a.done, b.done);  // true true

// 输出结果
// node main.js
b.js: a.done = false
b.js执行完毕
a.js: b.done = true
a.js执行完毕
main.js: a.done = true, b.done = true
复制代码

这是《es6入门》里的循环依赖的例子,这个例子能提现 commonjs 运行时加载的情况。因为 a.js 依赖 b.js,b.js 又依赖 a.js,所以当 b.js 执行到 require('./a') 的时候,a.js 会暂停执行,所以此时 require('./a') 返回的是false,但是在main.js中,a.js的返回值又是 true ,所以这说明了 commonjs 模块的 exports 是动态执行的,具体 require 能获取到的值,取决于模块的运行情况。

下面是 esm 的循环依赖的例子:

// a.mjs
export let a_done = false;
import { b_done } from './b';
console.log('a.js: b.done = %j', b_done);
console.log('a.js执行完毕');

// b.mjs
import { a_done } from './a';
console.log('b.js: a.done = %j', a_done);
export let b_done = true;
console.log('b.js执行完毕');

// main.mjs
import { a_done } from './a';
import { b_done } from './b';
console.log('main.js: a.done = %j, b.done = %j', a_done, b_done);

// 输出结果
// node --experimental-modules main.mjs
ReferenceError: a_done is not defined
复制代码

这里解释一下,为什么 a_done is not defined 。a.mjs 加载 b.mjs,而 b.mjs 又加载 a.mjs,这就形成了循环依赖。循环依赖产生时,a.mjs 中断执行,这时在 b.mjs 中 a_done 的值是什么呢?这就要考虑到 a.mjs 的 import/export 提升的问题,a.mjs 中的 export a_done 被提升到顶部,执行到 import './b' 时,执行权限移交到 b.mjs,此时 a_done 只是一个指定导出的接口,但是未定义,所以出现引用报错。

这里先提一下,如果用 babel 来编译执行,是不会报错的,执行结果如下:

// npx babel-node src/main.mjs
b.js: a.done = undefined
b.js执行完毕
a.js: b.done = true
a.js执行完毕
main.js: a.done = false, b.done = true
复制代码

为什么呢?后面会来分析。

bebel 模拟 esm

这一节来看看,babel 是怎么实现 esm 这几个特性的:输出引用、read-only。

还是上面的例子,稍微改一下:

// a.js
let a = 1;
let b = { num: 1 }
setTimeout(() => {
    a = 2;
    b = { num: 2 };
}, 200);
export {
    a,
    b,
};

// main.js
import {a, b} from './a';
console.log(a);
console.log(b);
setTimeout(() => {
    console.log(a);
    console.log(b);
}, 500);

a = 3;
复制代码

用babel编译一下,生成了如下的内容:

// a.js
"use strict";
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.b = exports.a = void 0;
var a = 1;
exports.a = a;
var b = {
  num: 1
};
exports.b = b;
setTimeout(function () {
  exports.a = a = 2;
  exports.b = b = {
    num: 2
  };
}, 200);

// main.js
"use strict";
var _a = require("./a");
console.log(_a.a);
console.log(_a.b);
setTimeout(function () {
  console.log(_a.a);
  console.log(_a.b);
}, 500);
_a.a = (3, function () {
  throw new Error('"' + "a" + '" is read-only.');
}());
复制代码

简单分析一下,对于输出引用,babel 是通过在输出属性变化时,同步修改 exports 对象对应的属性来实现的,比如像这样的代码:

exports.a = a = 2;

另外一个特性 read-only,babel 通过抛异常的方式来实现,比如这样的代码:

_a.a = (3, function () {
  throw new Error('"' + "a" + '" is read-only.');
}());
复制代码

bebel 模拟 esm 的局限

前面关于 esm 的 import/export 提升的例子,在 node 原生 esm 环境下和babel 编译环境下的执行结果不一致,这是什么原因呢?我们把前面的例子用 babel 编译一下,看看转换成什么形式的代码。

首先还是贴一下 esm 代码:

// a.mjs
export let a_done = false;
import { b_done } from './b';
console.log('a.js: b.done = %j', b_done);
console.log('a.js执行完毕');

// b.mjs
import { a_done } from './a';
console.log('b.js: a.done = %j', a_done);
export let b_done = true;
console.log('b.js执行完毕');

// main.mjs
import { a_done } from './a';
import { b_done } from './b';
console.log('main.js: a.done = %j, b.done = %j', a_done, b_done);
复制代码

用 babel 编译一下,生成了如下的内容:

// a.js
"use strict";
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.a_done = void 0;
var _b = require("./b");
var a_done = false;
exports.a_done = a_done;
console.log('a.js: b.done=%j', _b.b_done);
console.log('a.js执行完毕');

// b.js
"use strict";
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.b_done = void 0;
var _a = require("./a");
console.log('b.js: a.done=%j', _a.a_done);
var b_done = true;
exports.b_done = b_done;
console.log('b.js执行完毕');

// main.js
"use strict";
var _a = require("./a");
var _b = require("./b");
console.log('main.js: a.done=%j, b.done=%j', _a.a_done, _b.b_done);
复制代码

可以看到,babel 也实现了 export 的提升,输出值统一设置为 void 0 ,但是想象一下, a_done 其实是 export 对象的属相,那么在 commonjs 的环境下,从对象取值,只可能会出现 undefined ,而不可能出现 is not defined

其实根本原因也是源于 commonjs 输出的是对象,而 esm 输出的是引用,babel 本质是利用 commonjs 来模拟 esm,所以这个特性也是 babel 无法模拟实现的。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Haskell

Haskell

Simon Thompson / Addison-Wesley / 1999-3-16 / GBP 40.99

The second edition of Haskell: The Craft of Functional Programming is essential reading for beginners to functional programming and newcomers to the Haskell programming language. The emphasis is on th......一起来看看 《Haskell》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

html转js在线工具
html转js在线工具

html转js在线工具