文章目录
一、什么是循环依赖?
当两个或者多个模块互相直接或者间接引用时,就会形成循环依赖。例如:
bash
A.js → 依赖 → B.js
↑ ↓
← 依赖 ←
这种场景下模块的加载顺序会打破常规,导致意外结果。
二、循环依赖的典型表现
1.案例代码
bash
// a.js
const b = require('./b');
console.log('模块A 加载的B:', b);
module.exports = { value: 'A' };
// b.js
const a = require('./a');
console.log('模块B 加载的A:', a);
module.exports = { value: 'B' };
2.运行结果
bash
$ node a.js
模块B 加载的A: {} # 空对象!
模块A 加载的B: { value: 'B' }
3.原理分析
- Node.js 加载
a.js
时开始初始化模块A - 遇到
require('./b')
时暂停A,开始加载b.js
- 加载
b.js
时遇到require('./a')
,此时A模块尚未完成初始化 - 返回A模块当前状态(空对象{})
- B模块完成加载后,继续初始化A模块
三、解决方案
方案1:重构代码消除循环(最优解)
核心原则:重新设计模块结构,打破循环链
重构案例
bash
项目结构:
-src/
├── a.js
└── b.js
+src/
├── a.js
├── b.js
└── shared.js # 提取公共逻辑
bash
// shared.js
module.exports = {
commonLogic: () => { /* ... */ }
};
// a.js
const shared = require('./shared');
const b = require('./b');
// 使用 shared 和 b...
// b.js
const shared = require('./shared');
// 使用 shared...
方案2:延迟加载(当重构困难时)
核心思路:在需要时再加载依赖模块
修改后的 b.js
bash
// b.js
let a; // 先声明变量
function initA() {
a = require('./a'); // 延迟加载
}
module.exports = {
value: 'B',
getA: () => {
if (!a) initA();
return a;
}
};
使用方式
bash
// a.js
const b = require('./b');
console.log(b.getA().value); // 正确获取'A'
方案3:依赖注入(高级技巧)
核心思想:通过参数传递依赖,而非直接 require
改造后的模块
bash
// a.js
module.exports = function(b) {
return {
value: 'A',
bValue: b.value
};
};
// b.js
module.exports = function() {
return {
value: 'B',
// 稍后注入a
};
};
// main.js
const b = require('./b')();
const a = require('./a')(b);
b.a = a; // 完成双向关联
四、如何检测循环依赖
1.命令行检测
bash
node --trace-warnings your_app.js
# 出现警告时显示堆栈跟踪
2.使用 madge 生成依赖图
bash
npm install -g madge
madge --circular src/
五、循环依赖的隐藏危害
即使代码看似能运行,循环依赖仍可能导致:
- 不可预测的状态
- 内存泄漏:模块缓存无法释放
- 测试困难:模块间耦合度过高