Appearance
实现的不同
本节目标:理解“实现的不同”的核心思路,并能把它用于实际开发或面试表达。 底层实现上,CommonJS 和 ESModule (ESM) 的主要区别在于模块解析、加载、缓存和执行的机制不同。下面我们深入讲讲这两者的底层实现区别:
1. 加载机制CommonJS:运行时加载
- require 是一个函数,在代码运行时被调用。
- 加载模块时,Node 会同步读取文件内容,包装成函数执行(见下文)。
- 缓存的是模块的 exports 对象。
底层流程: // require('foo') 背后的流程1. 解析模块路径2. 读取文件内容3. 包裹一层函数:(function(exports, require, module, __filename, __dirname) { ... })4. 执行这个函数,并传入 require 等参数5. 返回 module.exports
ESModule:编译时加载(静态)
- import 是静态语法,在模块解析阶段就会确定依赖关系。
- 加载是异步的,先分析依赖图,再执行代码。
- ESModule 支持循环引用的实时绑定(live bindings)。
底层流程:
- 解析模块路径(支持 import map)2. 预扫描所有 import/export(构建依赖图)3. 确定模块之间依赖顺序4. 初始化模块(但不执行代码)5. 按依赖顺序执行模块
2. 包装机制CommonJS
- Node 会将每个模块包裹成如下函数:(function (exports, require, module, __filename, __dirname) { // 模块源码});
- 相当于每个文件是一个闭包,形成模块作用域。
ESModule
- 不需要函数包裹,天然具有模块作用域。
- 通过 V8 的Module Record 实现,每个模块是一个记录,存储导入导出等信息。
3. 缓存机制
| 特性 | CommonJS | ESModule |
| 缓存位置 | require.cache | 内部模块记录表 |
| 缓存对象 | module.exports | Module Namespace Object |
| 执行时机 | 首次 require 就立即执行 | 所有依赖扫描完后,再按顺序执行 |
4. 导出方式CommonJS
- 值拷贝(不是 live binding):exports.a = 1;setTimeout(() => exports.a = 2, 1000);另一个模块 require 后,拿到的是 { a: 1 },不会变。
ESM
- 实时绑定(live binding):export let a = 1;setTimeout(() => a = 2, 1000);import 这模块的另一个模块访问 a 会实时拿到最新值。
总结
| 项目 | CommonJS | ESModule |
| 加载时机 | 运行时加载 | 编译阶段静态分析 |
| 执行机制 | 同步,立即执行 | 异步,按依赖图顺序执行 |
| 包装方式 | 包裹成函数(Function Wrapper) | 原生模块作用域(Module Record) |
| 导出机制 | 值拷贝 | 实时绑定(Live Binding) |
| 缓存 | require.cache,缓存 exports | 模块记录,缓存 namespace 对象 |
| 支持循环引用 | 支持但值固定 | 支持并保证导出实时更新 |
如果你在研究 Node.js 内部实现或准备构建模块打包器(如 Webpack 插件),这些区别尤为重要。如果你想深入某一块(比如 module cache、循环引用处理、V8 的 Module 实现等),我可以继续扩展。 > 来自 <https://chatgpt.com/c/6805aef6-e7b4-8004-ab43-9a137f666985>
