Nestjs 循环依赖的实现

378 阅读2分钟

先从创建依赖实例的方法开始

// core/injector/instance-loader.ts
public async createInstancesOfDependencies(
  modules: Map<string, Module> = this.container.getModules(),
) {
  this.createPrototypes(modules);
  await this.createInstances(modules);
}

可以清晰的看出创建的过程分两步:

  • createPrototypes: 通过 Object.create() 创建一个对象
  • createInstances: 寻找依赖并创建实例

因为 provider, injectable, controller 的依赖注入实例化过程都大同小异,为方便解释,下面以 provider 为例子来说明依赖注入和实例化的过程

第一步

遍历所有模块的 provider ,并使用 Object.create() 进行创建。不会进行依赖注入,创建出来的对象也没有调用构造函数,是不完整的,一些属性也是 undefined ,这一步是为解决循环依赖作准备

第二步

这是依赖注入的核心。遍历所有模块的 provider ,从它们构造函数的参数和使用了 @Inject 装饰器的属性开始,进行深度优先遍历,从叶子结点(所有依赖都已经实例化或没有依赖的 provider )开始实例化整颗依赖树。但遇到循环依赖时就出现问题了,无法找到叶子结点,深度优先遍历会陷入循环。 解决方法是将循环依赖的双方都标记上 forwardRer ,当遇到 forwardRef 时,也当作叶子结点,但不对其进行实例化,而是返回第一步用 Object.create() 创建的对象。此时注入的这个对象是没有调用构造函数的,是不完整的,所以当出现循环依赖时,要注意构造函数中对这个依赖的使用,如果使用到一些需要完整实例化后才具备的属性时会有 undefined 的情况。

注意:之所以要在依赖双方都标记上 forwardRef 是因为我们无法确定在遍历时会先遇到哪个。

当所有的依赖都找齐时(有循环依赖关系的依赖此时是不完整的),就会使用 new 调用构造函数来实例化。被标记了 forwardRefprovider 实例化过程和一般的不同,它们调用构造函数后,会用创建出来的实例去覆盖先前第一步创建的对象,这样先前注入了不完整对象的地方也会变得完整,下面是实例化的主要代码:

instanceHost.instance = wrapper.forwardRef
  ? Object.assign(
    instanceHost.instance,
    new (metatype as Type<any>)(...instances),
  )
  : new (metatype as Type<any>)(...instances);

简化的循环依赖初始化过程

class A {
  constructor(b) {
    this.b = b;
    console.log(this);
  }
}

class B {
  constructor(a) {
    this.a = a;
    console.log(this);
  }
}

function main() {
  const instanceA = Object.create(A.prototype);
  const instanceB = Object.create(B.prototype);
  
  Object.assign(instanceA, new A(instanceB));
  Object.assign(instanceB, new B(instanceA));

  console.log(instanceA);
  console.log(instanceB);
}

main();

输出:

A { b: B {} }
B { a: A { b: B {} } }
<ref *1> A { b: B { a: [Circular *1] } }
<ref *1> B { a: A { b: [Circular *1] } }