网站首页 > 技术文章 正文
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
自从 ES6 推出以来,JavaScript 代码结构和组织发生了重大变化。其为开发者提供了众多新功能,比如:编写更易于重用和管理的代码,以模块系统为要。然而,在 ES6 之前,CommonJS 占据了主导地位,仍然被广泛使用,尤其是在 Node.js 中。
要编写好的代码则必须了解 ES6 和 CommonJS import 之间的核心差异。这篇博文将探讨两者之间的几个差异,并研究操作原理以及各自的优点和局限性。
1. ES6 静态 vs. CommonJS 运行时 import 解析
1.1 ES6 静态 import 解析
- ES6 import 在解析模块时解析,此时 代码并未执行
- 模块加载器 分析 import 语句并将其解析为相应的模块
- 由于 import 语句中的路径是静态评估的,支持静态分析 (Statically Analyzable) 和优化。而静态分析是指在不执行代码的情况下分析代码的过程,通常在编译或解析阶段,这也意味着无法包含任何运行时信息
// 不支持
import something from './utils/' + variableName;
- 通过捆绑器实现高效的 Tree-Shaking(删除未使用代码的优化技术) 和 Dead Code Elimination(死代码消除)。后者用于编译器通过分析代码路径,识别出那些由于条件永远不会满足或逻辑上不会被执行的代码块,用于 UglifyJS、Terser 等压缩工具中
- 支持 Dynamic import,可以在运行时确定模块路径,其返回一个 resolve 为模块对象的 Promise
////////////// 支持不同环境导入 ///////////
let myModule;
if (typeof window === "undefined") {
myModule = await import("module-used-on-server");
} else {
myModule = await import("module-used-in-browser");
}
/////////// 支持动态变量导入 //////////
Promise.all(
Array.from({length: 10}).map(
(_, index) => import(`/modules/module-${index}.js`),
),
).then((modules) => modules.forEach((module) => module.load()));
1.2 CommonJS 运行时 import 解析
针对 CommonJS 来说,其与 ESM 在模块处理方面有诸多不同。
- 模块导入在运行时解析,即代码执行时,这意味着 模块在程序运行时以阻塞方式加载和执行
- 模块的路径是根据运行时 require 语句的值动态确定的
const myModule = 'Module1';
const Modules = require(`../path/${myModule}`)
- 允许条件导入,即 require 语句可以在代码中的任何位置声明
const v = Math.random();
let myModule = {};
if (v> 0.5) {
myModule = require("./moduleA.cjs");
} else {
myModule = require("./moduleB.cjs");
}
console.log("模块值:", myModule);
- 打包器的静态分析和优化面临更大的挑战
2. ESM 实时绑定 (Live Bindings) 与导出引用
在 JavaScript 中,实时绑定和缓存值 的概念分别与 ES6 模块(import/export)和 CommonJS 模块(require/module.exports)中导入的处理方式相关。
在 ES6 模块中,import 会创建实时绑定 。这意味着 import 的值本质上是对原始导出值的引用。如果导出的值发生变化,import 的值将反映该变化。相当于在 import 值和导出值之间建立了动态链接,允许实时更新。
// moduleA.mjs
export let count = 0;
export function increment() {
count++;
}
// moduleB.mjs
import {count, increment} from './moduleA.js';
console.log(count);
// 输出 0
increment();
console.log(count);
// 输出 1
在 CommonJS 模块中,导入模块时将执行并缓存模块的值。如果导出模块使用的是对象,那么 CommonJS 的导入与 ESM 的 live-binding 非常类似。但是,如果导出的不是对象,配合 require 解构后将不再有 live-bindings 效果。
// 情况 1:count 是基础类型,配合解构后 live-bindings 不生效
const {count, increment} = require('./counter.cjs');
// 情况 2:count 是基础类型,如果不解构依然有 live-bindings 效果
const lib = require('./counter.cjs');
console.log(lib.count)
lib.increment()
console.log(lib.count)
// 情况 3:count 是引用类型,配合解构具有 live-bindings 效果
const {count, increment} = require('./counter.cjs');
console.log(count.value);
但是,总体来看,CommonJS 导出的依然是值的引用而非值的拷贝,这一点和网上甚嚣尘上的观点南辕北辙。
这个结论在我的另外一篇文章《到底什么是 live-bindings 中有详细论述》
3. ESM 和 CommonJS 的性能对比
在比较 ESM 和 CommonJS 导出的性能因素时,需要综合考虑以下因素:
- ESM 异步非阻塞加载 vs. CommonJS 同步阻塞解析
ES6 模块是异步解析,可与其他模块并行获取,从而减少初始加载时间。而 CommonJS 模块是同步解析,其会阻止后续代码执行,直到模块完全加载(类比 Node.js)。因此可能会导致初始加载时间变慢,尤其是在需要加载多个大型模块时。
这里的并行加载可以类比 Vite 的开发服务器,多个 import 并行加载,除非触发浏览器同域名资源并行加载上限。
// 两个文件同时加载
import {functionA} from './moduleA.js';
import {functionB} from './moduleB.js';
functionA();
functionB();
- ESM 支持依赖项静态解析
ES6 模块的静态特性允许在构建过程中更有效地解析依赖项。可以在编译时分析和解析依赖项,从而使捆绑器等工具能够优化捆绑包大小并消除未使用的 import。
// ESM 静态导入声明
import defaultExport from "module-name";
import * as name from "module-name";
import {export1} from "module-name";
import {export1 as alias1} from "module-name";
import {default as alias} from "module-name";
import {export1, export2} from "module-name";
import {export1, export2 as alias2, /* … */} from "module-name";
import {"string name" as alias} from "module-name";
import defaultExport, {export1, /* … */} from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";
相比之下,CommonJS 模块是在运行时动态解析的。这种动态解析会产生少量性能开销,因为 模块解析过程涉及在文件系统或缓存中搜索所需的模块。
- CommonJS 的 Tree-Shaking 比 ESM 更加复杂
ES6 模块支持静态分析,使 Tree-Shaking 能够通过删除未使用的代码来减小捆绑包大小并提高性能。相比之下,CommonJS 模块在定义上更加动态,因此分析起来困难得多。例如,ES 模块中的导入位置始终是字符串,而 CommonJS 中的导入位置则可能是表达式。打包器从 CommonJS 模块中消除未使用的导出非常困难,从而导致打包文件变大。
不过,像 @
rollup/rollup-plugin-commonjs 等插件可以通过将 CommonJS 模块先转换为 ESM 模块,然后也能在构建阶段利用 Tree-Shaking 的诸多能力。不过,相比于原生 ESM,一方面并非所有模块都能成功转化为 ESM,另一方面大量使用 CommonJS 也会导致打包时间明显变长。
如果无法转换为 ESM ,则 rollup 需要添加辅助函数(也可以理解为运行时)来模拟 CommonJS 的行为。
参考资料
https://medium.com/@rahul.jindal57/es6-imports-vs-commonjs-imports-8e9b66aa04bd
https://jameshfisher.com/2020/09/28/javascript-live-bindings-are-just-concatenation/
https://stackoverflow.com/questions/29168433/how-to-perform-a-variable-es6-import
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import
https://stackoverflow.com/questions/52965907/what-is-the-meaning-of-static-import-in-es6
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
https://web.dev/articles/commonjs-larger-bundles
https://github.com/rollup/rollup-plugin-commonjs/issues/362
- 上一篇: #前端高手进阶#一起薅羊毛~
- 下一篇: App架构设计经验谈:技术选型
猜你喜欢
- 2025-05-15 #前端高手进阶#一起薅羊毛~
- 2025-05-15 前端基础进阶(十):深入详解函数的柯里化
- 2025-05-15 2025 年 Object 和 Map 如何选择?
- 2025-05-15 为何说 postMessage 才是真正的 setTimeout(0)?
- 2025-05-15 为什么高手写 JS 总是又快又好?这10个技巧你要知道
- 2025-05-15 2025 年 Deno 终于官宣 pnpm 和 Yarn 可使用 JSR?
- 2025-05-15 宇宙厂:为什么前端要了解 Interaction to Next Paint (INP)
- 2025-05-15 Node.js 原生支持 TypeScript?开发者需要了解的一切
- 2025-05-15 请务必用 postTask/isInputPending 释放JS主线程!
- 2025-05-15 未来 JS 标准中的 Map.getOrInsert:彻底告别 if-else 判断?
你 发表评论:
欢迎- 590℃几个Oracle空值处理函数 oracle处理null值的函数
- 584℃Oracle分析函数之Lag和Lead()使用
- 570℃0497-如何将Kerberos的CDH6.1从Oracle JDK 1.8迁移至OpenJDK 1.8
- 569℃Oracle数据库的单、多行函数 oracle执行多个sql语句
- 564℃Oracle 12c PDB迁移(一) oracle迁移到oceanbase
- 557℃【数据统计分析】详解Oracle分组函数之CUBE
- 542℃最佳实践 | 提效 47 倍,制造业生产 Oracle 迁移替换
- 537℃Oracle有哪些常见的函数? oracle中常用的函数
- 最近发表
- 标签列表
-
- 前端设计模式 (75)
- 前端性能优化 (51)
- 前端模板 (66)
- 前端跨域 (52)
- 前端缓存 (63)
- 前端react (48)
- 前端aes加密 (58)
- 前端脚手架 (56)
- 前端md5加密 (54)
- 前端路由 (61)
- 前端数组 (73)
- 前端js面试题 (50)
- 前端定时器 (59)
- 前端懒加载 (49)
- Oracle RAC (73)
- oracle恢复 (76)
- oracle 删除表 (48)
- oracle 用户名 (74)
- oracle 工具 (55)
- oracle 内存 (50)
- oracle 导出表 (57)
- oracle 中文 (51)
- oracle的函数 (57)
- 前端调试 (52)
- 前端登录页面 (48)
本文暂时没有评论,来添加一个吧(●'◡'●)