commonjs 与 esm 的区别 您所在的位置:网站首页 ansys20和19的区别 commonjs 与 esm 的区别

commonjs 与 esm 的区别

2023-03-21 22:45| 来源: 网络整理| 查看: 265

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

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

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

输出拷贝 vs 输出引用 esm 的 import read-only 特性 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 无法模拟实现的。

结论

本文主要总结了 commonjs 跟 esm 的主要对比,并且分析了 babel 模拟 esm 的方式和局限。

文章主要是个人的理解和总结,如有错误欢迎指正。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有