什么是循环依赖
简介
循环依赖是一个很简单的概念,但又是一个很容易想当然的概念。
简单理解就是 A 依赖 B,同时 B 又依赖 A,所以 A 和 B 构成循环依赖。
这样的理解不能说是错误的,只能说是没有意义的。听君一席话,如听一席话。
示例 1
// 这是 A 文件的代码。
import { B } from './B.ts';
export const A: number = 100;
export function getAValue() {
return A + B;
}
// 这是 B 文件的代码。
import { A } from './A.ts';
export const B: number = 200;
export function getBValue() {
return A + B;
}
这个例子展示了 A,B 两个模块互相依赖,但确实没有产生循环依赖的问题。 具体来说就是互相 import 并不会产生循环依赖问题,代码可以正常运行。
示例 2
// 这是 A 文件的代码。
import { B } from './B.ts';
export const A: number = B + 100;
// 这是 B 文件的代码。
import { A } from './A.ts';
export const B: number = A + 100;
这个例子比上一个例子更简单,但却产生了循环依赖问题。 这是因为上一个例子中import {A}
或者import {B}
都是可以立即返回数据的。 但是本例子中import {A}
或者import {B}
都是需要有一个计算过程,而计算过程中又需要依赖对方,此时就产生了循环依赖。
示例 3
// 这是 A 文件的代码。
import { B } from './B.ts';
export class A {
@Inject(B)
public b: B;
}
// 这是 B 文件的代码。
import { A } from './A.ts';
export class B {
@Inject(A)
public a: A;
}
这个例子也会产生循环依赖问题。只需要理解@Inject
属于立即执行函数,也就是在 import 时会立即执行装饰器。 那么在 A 文件中import {B}
时,B 又会立即需要import {A}
的执行结果。但是import {A}
又是依赖import {B}
的,所以产生了循环依赖。 实际上本例子和上一个例子没有本质上区别,上一个例子也是为本例子做铺垫,方便大家理解装饰器的执行时机。
示例 4
// 这是 A 文件的代码。
import { B } from './B.ts';
export class A {
@Inject(new LazyToken(() => B))
public b: B;
}
// 这是 B 文件的代码。
import { A } from './A.ts';
export class B {
@Inject(new LazyToken(() => A))
public a: A;
}
这个例子和上一个例子的唯一区别是引入了 LazyToken,但却是能够解决循环依赖的问题。 这是因为@Inject
在执行时,变成依赖new LazyToken
了,而且new LazyToken
的参数也变成了一个函数。 参考示例 1,这里的函数并没有执行,只时作为参数传递给new LazyToken
,所以并不影响 A 和 B 互相 import,也就是不会导致循环依赖。
示例 5
// 这是 A 文件的代码。
import { getBValue } from './B.ts';
export const A: number = 100;
export function getAValue() {
return A + getBValue();
}
// 这是 B 文件的代码。
import { getAValue } from './A.ts';
export const B: number = 200;
export function getBValue() {
return B + getAValue();
}
这个例子表面上看起来并没有循环依赖问题,AB 都可以正常互相 import。但是一旦运行getAValue
或者getBValue
函数,则会导致循环依赖问题。 这个例子展示了循环依赖问题不仅仅会出现在互相 import 时立即发生。也可能在运行特定代码时才会发生。
依赖注入中循环依赖问题分析
一种场景是上面的示例 3,也就是
@Inject
导致的循环依赖。此时本库可以通过LazyToken
来解决。在 inversify 中则是通过LazyServiceIdentifier
来解决。另一种场景则是上面的示例 5。是在运行时发生的循环依赖。一般是在类的实例化过程中发生的。也就是在执行
const a = container.get(A)
这行代码时,可能会发生循环依赖。查看 resolveInstanceValue 源代码
inversify 执行 resolveInstanceValue 的过程
获取构造函数参数依赖数组
new ClassName(...args)
获取所有属性注入依赖
执行PostConstruct逻辑
执行binding activation逻辑
执行container activation逻辑
存入cache
观察到存入cache
这一步是在最后一步,所以前面所有步骤都可能导致循环依赖。包括以下这些:
- 构造函数的参数导致的循环依赖
- 属性注入导致的循环依赖
- binding activation 逻辑导致的循环依赖
- container activation 逻辑导致的循环依赖
所以 inversify 从理论上就不可能原生支持任何循环依赖。
inversify 想要支持属性注入的循环依赖,只能通过第三方的 lazyInject 才能实现。这个库是通过延迟实例化属性来避免循环依赖的。
另一方面 inversify 的 LazyServiceIdentifier 只能解决 import 时的循环依赖问题,并不能解决container.get(A)
在实例化对象时的循环依赖问题。
本库执行 resolveInstanceValue 的过程
获取构造函数参数依赖数组
new ClassName(...args)
执行binding activation逻辑
执行container activation逻辑
存入cache
获取所有属性注入依赖
执行PostConstruct逻辑
观察到存入cache
这一步是在倒数第3步,所以前面所有步骤都可能导致循环依赖。包括以下这些:
- 构造函数的参数导致的循环依赖
- binding activation 逻辑导致的循环依赖
- container activation 逻辑导致的循环依赖
注意这里获取所有属性注入依赖
是在存入cache
之后,所以本库默认就是支持属性注入的循环依赖的。 因为如果确实有属性注入的循环依赖,也可以从 cache 中提前访问到已经实例化的对象,从而可以解决循环依赖问题。
PostConstruct 必须在属性注入之后,因为 PostConstruct 方法中大概率会访问这些注入的属性。
本库方案的已知缺陷是会导致 activation 逻辑中不能访问注入的属性,因为此时属性还没有完成注入流程。
其他方案
下面列举了 5 种其他方案,但是都不能满足需求或者存在其他问题。
存入cache
必须在 activation 之后,因为 activation 的返回值需要影响最终 cache 的值。存入cache
这一步不能太晚,否则就不能解决属性注入导致的循环依赖问题。如果想要属性注入可以支持循环依赖,那么属性注入依赖就必须在存入 cache
之后。如果是分成两步
存入cache
和更新cache
。
那么这里必须要求 activation 只能 mutable 修改 cache 中的对象,而不能替换成新的对象。 因为如果替换成新对象,就会导致已经读取了 cache 中旧对象的代码一直引用的都是旧对象,而不是更新 cache 之后的新对象,最终导致逻辑异常。
就算 activation 只能 mutable 修改原始对象,不能返回新对象,也就是不更改 cache 的引用,也是不能提前初始化 cache 的。 因为从语义上来说必须等待 activation 执行完毕,其他服务才能开始访问 cache。否则就是访问了未初始化完毕的数据,也会导致逻辑异常。
当然,inversify 做的更加极致,必须等待属性注入,PostConstruct,binding activation,container activation 依次执行完毕之后才会初始化 cache。 可以说这个流程是最符合语义的,或者说最符合大多数人理解的流程。但是这么做导致的最大问题就是完全不支持属性注入的循环依赖了。
其他方案 1
获取构造函数参数依赖数组
new ClassName(...args)
存入cache
获取所有属性注入依赖
执行PostConstruct逻辑
执行binding activation逻辑
执行container activation逻辑
更新cache
其他方案 2
获取构造函数参数依赖数组
new ClassName(...args)
获取所有属性注入依赖
执行PostConstruct逻辑
存入cache
执行binding activation逻辑
执行container activation逻辑
更新cache
其他方案 3
获取构造函数参数依赖数组
new ClassName(...args)
存入cache
执行binding activation逻辑
执行container activation逻辑
更新cache
获取所有属性注入依赖
执行PostConstruct逻辑
其他方案 4
获取构造函数参数依赖数组
new ClassName(...args)
执行binding activation逻辑
执行container activation逻辑
获取所有属性注入依赖
存入cache
执行PostConstruct逻辑
其他方案 5
获取构造函数参数依赖数组
new ClassName(...args)
执行binding activation逻辑
执行container activation逻辑
获取所有属性注入依赖
执行PostConstruct逻辑
存入cache