模块化:从CommonJS到ESM,模块依赖循环引用的坑你踩过吗?

阿乐
阿乐 管理员 黑卡会员
发布于 2026-09-11 20:44 ·2 浏览 ·0 回复

第一次在 Node 项目里遇到循环依赖,我盯着一个 `undefined` 纳闷了半天:明明在 `a.js` 里导出了函数,`b.js` 却死活拿不到。后来才知道,这不是代码写错了,而是模块加载机制在循环引用时露出的“半成品”状态。从 CommonJS 到 ESM,模块化方案换了一代又一代,但循环依赖这个坑,依然有人前赴后继地踩。

CommonJS 的“半成品”导出

CommonJS 的 `require` 是同步加载、执行、缓存。当 `a.js` 依赖 `b.js`,而 `b.js` 又回头依赖 `a.js` 时,Node 不会报错,而是把 `a.js` 当前已经执行到的 `module.exports` 先交给 `b.js`。问题在于,此时 `a.js` 可能还没执行完,导出的对象可能只是个空壳。

// a.js
exports.done = false;
const b = require('./b');
console.log('in a, b.done =', b.done);
exports.done = true;

// b.js
exports.done = false;
const a = require('./a');
console.log('in b, a.done =', a.done); // false,而不是 true
exports.done = true;

`b.js` 里拿到的 `a` 是未完成的 exports,所以 `a.done` 还是 `false`。更隐蔽的是,如果 `a.js` 后来用 `module.exports = {...}` 整体替换,`b.js` 手里拿到的还是旧引用,永远不会更新。解决办法通常是把 `require` 延迟到函数内部,或者让模块只导出函数,不在顶层消费依赖。

ESM 的 live binding 不是万能药

ESM 看起来更“高级”:静态分析、依赖图先建立、再实例化、最后执行。导出是 live binding,值会联动。但循环依赖时,它照样能让你翻车,而且报错更晚、更难查。

// a.mjs
import { b } from './b.mjs';
export const a = 'a';
console.log(b);

// b.mjs
import { a } from './a.mjs';
export const b = a; // ReferenceError: Cannot access 'a' before initialization

因为 `const` / `let` 没有初始化提升,`b.mjs` 在顶层读取 `a` 时会撞上 TDZ。而函数声明有提升,所以循环依赖里“导出函数”往往能侥幸跑通,但函数内部如果访问了尚未初始化的变量,调用时依然可能炸。live binding 能保证你稍后拿到最新值,却不能保证“此刻”就能安全访问。

混用 CJS 和 ESM 更酸爽

Node 里 ESM 可以 `import` CommonJS,CommonJS 也能有限地 `require` ESM,但两者加载语义不同。一旦循环依赖跨越两种模块系统,默认导出、命名导出、缓存时机全搅在一起,排查难度直接翻倍。如果项目正在迁移,尽量统一模块系统;实在要混用,用动态 `import()` 打破同步循环,别让静态依赖图绕成死结。

排查与规避:让依赖图保持单向

工具能帮你省不少事:`madge --circular src` 可以扫出循环依赖,`eslint-plugin-import` 的 `no-cycle` 能在提交前报警,Webpack 也有 `circular-dependency-plugin`。但工具只是报警器,真正的解法在架构:

- 保持依赖单向,分层清晰,上层依赖下层,下层绝不反向依赖;
- 把共享类型、常量、工具函数抽到第三个模块;
- 用依赖注入、事件总线或注册表解耦;
- 慎用 barrel 文件,`index.ts` 互相导入是循环依赖的重灾区;
- 延迟加载:函数内 `require`,或动态 `import()`。

写在最后

循环依赖不是语言 bug,而是设计信号。CommonJS 给你一个未完成的 exports,ESM 给你 TDZ 和 live binding 的错觉,本质上都在提醒:模块之间的依赖关系开始纠缠了。理解加载顺序,保持依赖图尽量是一张有向无环图,比记住任何“奇技淫巧”都管用。下次再看到 `undefined` 或 `Cannot access before initialization`,先别急着改代码,画一画依赖图,坑可能就在那个环上。

本文转载自 阿乐技术社区,原文地址:https://www.leleweb.cn/thread-233.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~