> For the complete documentation index, see [llms.txt](https://1425816423.gitbook.io/my-knowledge-base/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://1425816423.gitbook.io/my-knowledge-base/qian-duan-ji-shu/qian-duan-gong-cheng-hua/xun-huan-yi-lai.md).

# 循环依赖

## 什么是循环依赖

循环依赖是模块化机制下的一种现象。循环依赖是指当模块a依赖于模块b，但是模块b又依赖于模块b的情况。ES Module和CommonJs两种模块化机制对循环依赖的处理是不相同。

可以用下面这个例子来理解循环依赖：

```tsx
// a.js
const obj = require("./b.js");

exports.a = 1;

console.log(obj);

// b.js
const { a } = require("./a.js");

const obj = {
	a,
};

module.exports = obj 
```

在上面的例子中，模块a引用了模块b的obj对象，但是模块b的obj对象的a属性却又来源于模块a，这就导致了两个模块互相依赖，产生了循环依赖。

### CommonJs中的循环依赖

上面这段代码在CommonJs模块化机制下的输出是`undefined` 。这是由于CommonJs的实现机制决定的。

对于上面的代码，我们想要的结果应当是`obj = {a: 1}`，但是实际上结果确是 `{a: undefined}` 。并且nodejs还会提示你出现了循环依赖。

在介绍具体的原理前我们需要知道几个知识点：

* CommonJs实现模块化的方式是利用对象的方式实现的，导入和导出的都是js对象
* require函数导入的实际上是另一个模块的exports对象，exports默认是空对象，即`{}`，并且exports和module.exports是同一个对象。

由此可以分析出，**当模块a执行到`require("./b.js")` 语句时，此时模块a的exports对象是空的，即`exports = {}`** ，这个时候js会执行模块b中的代码，当执行到`require("./a.js")` 时，由于模块a已经执行过了，所以不会再执行，而是直接返回模块a的exports对象，**前面分析过模块a的exports对象是空对象，因此`require("./a.js")` 返回的也是这个空对象，因此`a=undefined`** ，之后再将obj对象导出，模块a的`require("./b.js")` 就返回了`{a: undefined}` 。

你可以通过debugger的方式来分析此过程。

```tsx
// a.js
debugger;
// 执行到require函数调用语句时会立即进入模块b中执行代码
// 注意这里require()不会马上返回结果，要等模块b代码都执行完毕后才会返回模块b的exports对象
const obj = require("./b.js");
// 导出变量a
exports.a = 1;

console.log(obj);
// 结果： {a: undefined}

// b.js
// require("./a.js")返回的是模块a的exports的对象，由于此时模块a还未导出，因此a是undefined。
const { a } = require("./a.js");

const obj = {
	a,
};
// 模块b导出对象obj
module.exports = obj 
```

### ES Module中的循环依赖

同样是上面的例子，我们改写成ES Module(简称：ESM)的写法来看一下。

```tsx
// a.mjs
import obj from "./b.mjs";

export let a = 1;

console.log(obj);

// b.mjs
import { a } from "./a.mjs";

export default {
	a,
};

// node a.mjs
// ReferenceError: Cannot access 'a' before initialization
```

这次的结果和CommonJs中的结果不同，会直接报错，报错信息提示不能在变量a初始化之前访问。

**导致CommonJs和ES Module对循环依赖不同现象的原因是CommonJs是通过js对象来导入导出的，它的导入是在运行时确定的，require是一个函数，exports是一个对象；而ES Module(ESM)的导入导出(`import`、`export`)是一种JavaScript语法，ESM是静态的，它有一个静态编译阶段，它的导入导出是在编译时确定的。**

首先js引擎执行到`import obj from "./b.mjs";` 语句后会执行模块b的代码，在模块b中的又导入了模块a的变量a，此时js引擎会认为从模块a中导入的变量a已经定义好了，并不会去执行模块a的代码，而是继续执行模块b后面的代码，知道执行到`export default {a};` 时去访问变量b，才发现并没有导入成功，因此报错。

## 解决循环依赖

### 提前导出

在CommonJs中，由于导出导入都是通过js对象实现的，因此我们可以通过提前导出来避免出现循环依赖的问题。

```tsx
// a.js
// 导出语句先执行
exports.a = 1;

const obj = require("./b.js");

console.log(obj);
```

当然这里也有弊端，如果导出的变量取决于导入的值，那么就不能用这个方法。

```tsx
// a.js
// 当导出的是明确的值时，可以提前导出
// exports.a = 1;
// 但是如果导出的值取决于导入的值时，那么就会出问题了
exports.b = obj.b;
const obj = require("./b.js");
console.log(obj);

// b.js
const { a } = require("./a.js");

const b = 1;

const obj = {
	a,
	b,
};

module.exports = obj;

// node a.js
// Uncaught ReferenceError ReferenceError: Cannot access 'obj' before initialization
```

另外在ESM中也无法起作用。

### 利用函数提升

在ESM即使你把export写在import语法的前面，js引擎也仍然会先执行import语句，解决的方法是利用函数提升来实现提前导出。

```tsx
// a.mjs
import obj from "./b.mjs";

console.log(obj);

export function a() {
	return 1;
}

// b.mjs
import { a } from "./a.mjs";

export default {
	a: a(),
};
```

> 注意只有函数声明才会有函数提升，其他例如箭头函数、函数表达式等都没有函数提升现象，因此无法其作用。

**这种方法的思路仍然是提前导出，只不过它利用了js函数提升的特性实现**。而在CommonJs中由于导入导出都是通过对象完成的，因此可以直接吧导出语句写在导入语句的前面来实现。

### 优化模块逻辑

循环依赖很容易出现bug，在ESM下会报错提示，但是在CommonJs却不会有报错提示，只能靠开发者自己检测，这很容易出现意想不到的问题，因此最好的办法是优化模块逻辑来避免循环依赖的情况。
