模块循环引用
======
从 cjs/loader.js 的 L765、L772 和 L784 行代码可以看到,在模块执行前就会创建好对应的模块对象,并进行缓存。模块执行的过程实际是在给该模块对象计算需要导出的变量属性。因此,CommonJS 模块在启动执行时,就已经处于可以被获取的状态,这个特点可以很好地解决模块循环引用的问题。
如图 5 所示,模块 A 依赖模块 B,模块 B 又依赖模块 A,模块 A 和 B 分别被 require 语句从上往下分为 2 段,记为 A1、A2、B1、B2。
如图 6 所示,代码块的执行顺序为:A1 -> B1 -> B2 -> A2。
使用不当的问题
=======
如果 B2 使用了 A2 导出的变量会怎么样呢?模块 A 的模块对象上不存在该变量对应的属性,获取的值为 undefined。获得 undefined 虽然不符合预期,但一般不会造成 JS 错误。
可以看到,由于 require 语句直接分割了执行的代码块,CommonJS 模块的导入导出语句的位置会影响模块代码语句的执行结果。
ES6 模块
======
ES6 模块[4]借助 JS 引擎实现。JS 引擎实现了 ES6 模块的底层核心逻辑,JS 运行时需要在上层做适配。适配工作量还不小,比如实现文件的加载,具体可以看一下我发起的一个讨论[5]。
模块使用报错
======
ES6 模块使用不当时,由 JS 引擎或 JS 运行时的适配层抛出错误。比如:
// Node.js 中报错
internal/process/esm_loader.js:74
internalBinding(‘errors’).triggerUncaughtException
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find module
// 浏览器中报错
Uncaught SyntaxError: The requested module ‘./child.js’ does not provide an export named ‘b’
第一个是 Node.js 适配层触发的内部错误(不是通过 throw 抛出的),第二个是浏览器抛出的 JS 引擎级别的语法错误。
模块执行顺序
======
ES6 模块有 5 种状态,分别为 unlinked、linking、linked、evaluating 和 evaluated,用循环模块记录(Module Environment Records)[6]的 St