深入分析JavaScript模块循环引用

背景

大力教育的在线教室中台提供封装了核心能力的教室 SDK,业务方基于教室 SDK 开发面向用户的在线教室 App。最近对教室 SDK 做一次比较大的改动时,我遇到了一个懵逼的问题。这个问题耗费了我 3 天左右时间,让我压力一度大到全身发热。当时虽然解决了问题,但并没有很理解原因。直到一个多月后,才有时间做一些更深入的分析,并写下这篇文章。

当时的情况是,业务方 App 工程能通过 TypeScript 编译,但在运行时会报错。就不同的使用教室 SDK 的方式,报错有两种。图 1 为在业务方 App 工程里正常安装教室 SDK 后进行调试时的报错;图 2 为在业务方 App 工程里 yarn link 教室 SDK 后进行调试时的报错。

在分析这个问题前,需要先分析一下 JS(JavaScript)的模块机制。

CommonJS vs ES6 模块

CommonJS 与 ES6(ECMAScript 6)模块有什么区别呢?《ECMAScript 6 入门教程 》[1]一书在“Module 的加载实现”章节指出两个模块体系有三个重大差异。个人觉得这三个差异基本是错误的,给大家造成了不少误解。后面再讲质疑的理由,这里先抛出我总结的几点差异:

  • CommonJS 模块由 JS 运行时实现,ES6 模块借助 JS 引擎实现;ES6 模块是语言层面的底层的实现,CommonJS 模块是之前缺失底层模块机制时在上层做的弥补。从报错信息可以察觉这个差异。
  • CommonJS 模块同步加载并执行模块文件,ES6 模块提前加载并执行模块文件。CommonJS 模块在执行阶段分析模块依赖,采用深度优先遍历(depth-first traversal),执行顺序是父 -> 子 -> 父;ES6 模块在预处理阶段分析模块依赖,在执行阶段执行模块,两个阶段都采用深度优先遍历,执行顺序是子 -> 父。
  • CommonJS 模块循环引用使用不当一般不会导致 JS 错误;ES6 模块循环引用使用不当一般会导致 JS 错误。
  • CommonJS 模块的导入导出语句的位置会影响模块代码执行结果;ES6 模块的导入导出语句位置不影响模块代码语句执行结果。

为了方便说明,本文把 JS 代码的运行大致分为预处理和执行两个阶段,注意,官方并没有这种说法。下面进行更细致的分析。

CommonJS 模块

在 Node.js 中,CommonJS 模块[2]由 cjs/loader.js[3] 实现加载逻辑。其中,模块包装器是一个比较巧妙的设计。

在浏览器中,CommonJS 模块一般由包管理器提供的运行时实现,整体逻辑和 Node.js 的模块运行时类似,也使用了模块包装器。以下分析都以 Node.js 为例。

模块使用报错

CommonJS 模块使用不当时,由 cjs/loader.js 抛出错误。比如:

// Node.js
internal/modules/cjs/loader.js:905
  throw err;

  ^

Error: Cannot find module './none_existed.js'
Require stack:
- /Users/wuliang/Documents/code/demo_module/index.js

可以看到,错误是通过 throw 语句抛出的。

模块执行顺序

CommonJS 模块是顺序执行的,遇到 require 时,加载并执行对应模块的代码,然后再回来执行当前模块的代码。

如图 3 所示,模块 A 依赖模块 B 和 C,模块 A 被 2 个 require 语句从上往下分为 3 段,记为 A1、A2、A3。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值