什么是依赖注入

依赖注入(Dependency Ineject)是 IOC 思想的一种实现。它通过面向接口编程,将直接依赖,转为间接依赖,使得被依赖的类型实现,在发生变化的时候,只需要在 DI 容器中变更接口与具体实现的关系即可,而不用去变更每一个应用依赖的地方。

这里我们用经典的 Car 与 Engine 举例,在不使用 DI 的时候,我们的 Car 需要在内部创建一个新的 Engine 实例,类图如下。

Angular学习笔记——依赖注入(一) - 图1

我们用代码来实现它

  1. // Engine Interface
  2. interface Engine{
  3. drive():void
  4. }
  5. // 电力引擎
  6. class ElectricEngine implements Engine{
  7. drive(){ console.log("电力引擎")}
  8. }
  9. // Car Interface
  10. interface Car{
  11. drive()
  12. }
  13. // 特斯拉
  14. class Tesla implements Car{
  15. constructor(){
  16. this.engine = new ElectricEngine();
  17. }
  18. private readonly engine:ElectricEngine;
  19. driver(){
  20. this.engine.drive()
  21. }
  22. }

在这个实现里,我们为特斯拉安装了一个用电的引擎,随着后续的修改,如果出现了更清洁的能源,就需要更换新的引擎,此时我们就要回来 重新为特斯拉 new 一个新的引擎,这违反了开闭原则,当我们有多个新能源汽车实现类的时候,我们不得不挨个打开他们,去修改构造函数中引擎的实例。

现在我们来看看应用了依赖反转思想的实现,这里我们要说一下,依赖注入的方式,有两种,一种是通过属性的方式进行注入,而另一种则是通过构造函数进行注入,他们看起来像这样

  1. // 通过构造函数注入
  2. class ByConstructor{
  3. constructor(private readonly service:Service){}
  4. }
  5. // 通过属性注入
  6. class ByProperty{
  7. public service:Service;
  8. }

嗯 依赖注入的本质就是传参,把本应该由自己构造的对象,由外部传进来,实现解耦。下边我们来看看车的 依赖注入版本

Angular学习笔记——依赖注入(一) - 图2

我们用代码来实现它

  1. // Engine Interface
  2. interface Engine{
  3. drive():void
  4. }
  5. // 电力引擎
  6. class ElectricEngine implements Engine{
  7. drive(){ console.log("电力引擎")}
  8. }
  9. // Car Interface
  10. interface Car{
  11. drive()
  12. }
  13. // 特斯拉
  14. class Tesla implements Car{
  15. constructor(engine:Engine){
  16. this.engine = engine;
  17. }
  18. private readonly engine:Engine;
  19. drive(){
  20. this.engine.drive()
  21. }
  22. }
  23. // 使用的时候
  24. const tesla:Car = new Tesla(new ElectricEngine());
  25. tesla.drive()

到目前为止,我们看到的除了 Engine 提供的实际延迟到了构造 Car 的时候,并没有什么优势,反而增加了代码量,并且在 Engine 有新的实现的时候,还要再每一个 创建 Car 的地方都要重新修改。但是此时我们的 Tesla 实现类,已经是可以测试的了。为什么呢?因为我们可以再测试的时候,为 Car 提供一个我们自己设定好的,可控的 Mock Engine。

另一个构造函数进行传参的问题,则是 DI 框架要为我们解决的了,它解决了接口与实现类之间的映射关系,也解决了如何在构造一个实例的同时,如何 构造相关依赖的问题。

Angular 中的依赖注入

在 Angular 中,为我们提供了如下几个类/函数,帮助我们处理依赖注入的问题

  • @Injectable() 一个装饰器 用于标记一个类是可被注入的,这个类一定是个实现类
  • Injector 注入器,它维护了相关依赖的缓存,以及 接口与实现的 关联 Map
  • @Inject() 一个装饰器,用于指定从 Injector 中获取哪个接口的实现

还用我们刚刚的 Car 举例子,由于 JS 中没有接口的概念,因此我们替换成抽象类,它在 Angular 中的实现是这样的。

  1. // Engine Interface
  2. abstract class Engine{
  3. abstract drive();
  4. }
  5. // 可被注入的Engine实现
  6. @Injectable()
  7. class ElectricEngine implements Engine{
  8. drive(){ console.log ("电力引擎"); }
  9. }
  10. // Car Interface
  11. abstract class Car{
  12. abstrct drive()
  13. }
  14. // 可被注入的Car实现
  15. @Injectable()
  16. class Tesla implements Car{
  17. constructor(engine:Engine){this.engine=engine;}
  18. private engine:Engine
  19. drive(){ this.engine.drive();}
  20. }

到这里为止,我们只是提供了 2 个可以注入的实现,但是没有维护他们和接口之间的关系。如何进行维护呢?在 Angular 中,NgModule,Component,Directive 都为我们提供了 Providers,我们现在只关心在 NgModule 中的 Providers。它的应用方式如下。

  1. @NgModule({
  2. providers:[
  3. {provide:Engine,useClass:ElectricEngine},
  4. {provide:Car,useClass:Tesla},
  5. ]
  6. })
  7. export class AppComponent{
  8. }

在这里我们注册好了基于类的提供者,Angular 的 Injector(注入器),会帮我们创建它们的单例,并进行缓存。同样的,我们的 Car 现在也是可被注入的,下面我们在 AppComonent 里注入我们的 Car

  1. @Component({
  2. template:`<h1>一个Car的依赖注入Demo</h1>`,
  3. })
  4. export class AppComponent{
  5. constructor(car:Car){
  6. car.drive();
  7. }
  8. }

在这里,我们可以看到最终的输出 “电力引擎”,这表示注入器已经帮我们构建好了 ElectricEngine 与 Car 的实例,并且将 EletricEngine 实例注入到了 Car 中。

依赖注入的特性

依赖注入是懒惰的

我们在 NgModule 的 Providers 属性中,虽然提供了 Car 和 Engine 的实现,但是如果代码中并没有对 Car 的依赖的情况下,注入器不会为我们创建 Car 和 Engine 的实现

被注入的实例都是单例

在每一个 Injector(注入器)中,接口的实例都是单例的,会被缓存在 Injector 中。有时候我们可能会看到多个实例,这是因为 Injector 可能会存在多个。

Injector 是棵树

刚刚我们说过,Injector 可能有多个,这是因为每一个懒加载的 NgModule,每一个 Component,都会有一个新的 Injector 实例,他们会设置上一级的 Injector 为父级 Injector,形成树形结构。

为什么 Injector 是树形结构呢? 因为 Angular 的 DI 支持向上递归查找依赖。

比如当前的注入器中没有某个 Service 的实现,那么我就会往上找,直到找到某一级的实现并返回,或者找到头也没有,就直接返回 NullInjector 了。

然而 Injector 不但是棵树,而且它还是两颗树,一颗树的根叫 ModuleInjector,另一个棵树,叫 ElementInjector,我们一会会在后边分析他们。他们这俩树 大概长这样。

Angular学习笔记——依赖注入(一) - 图3

我们在查找依赖的时候,会优先从 ElementInjector 树中进行寻找,如果没有,则会转到隔壁 ModuleInjector 中进行查找,要是都没有,就返回个 NullInjector,如果当前不是可选的依赖的话,就会报错。

ModuleInjector 与 ElementInjector

ModuleInjector,顾名思义,就是 NgModule 为我们创建的 Injector。这个 Injector 树比较简单,只有在懒加载模块中,才会产生新的子注入器实例。

ElementInjector,是为我们的组件提供的,它挂在组件所在 Module 的 ModuleInjector 下边。无论是否提供 Provider,我们的组件都会为我们生成一个 ElementInjector,并且将组件自身实例注入进去。 所以当我们在组件的 Providers 里提供实现类的时候,每一个组件都会产生一个新的实例。

他们实际看起来像是这样

Angular学习笔记——依赖注入(一) - 图4

Provider

provider,是依赖注入的提供者,它支持一下几种方式提供实现

  • useValue 直接使用一个值
  • useClass 使用一个可被注入的类,在存在依赖的时候,会创建该类的单例
  • useFactory 提供一个工厂方法,用于在运行时动态产生实例
  • useExisting 使用一个已存在的实例,这个实例仅限于当前 NgModule 的 providers 中

它的 provider 要求是一个类/抽象类/InjectionToken,不能使用 interface,number,string 这些,是因为要么 JS 中没有,要么不能确认唯一。

他们的使用方式如下

  1. // Injectoken
  2. const A_VALUE = new InjectionToken("a_value");
  3. const A_EXIST = new InjectionToken("a_exist");
  4. @NgModule({
  5. providers:[
  6. {provide:Engine,useClass:ElectricEngine},
  7. {provide:A_VALUE,useValue:1},
  8. {provide:Car,useFctory:(engine:Engine)=>{
  9. if(engine instanceof ElectricEngine){
  10. return ()=> new Car(engine);
  11. } else{
  12. return ()=> null
  13. }
  14. },deps:[Engine]}
  15. {provide:A_EXIST,useExist:forwardRef(()=>Engine)},
  16. ]
  17. })
  18. export class AppComponent{
  19. }

我们在使用 useExisting 的时候,通常会在前面加一个 forwardRef() 这是为了避免存在依赖的循环引用,比如 A 引用了 B,B 又引用了 A 这种情况。

限定符

Angular 为我们提供如下几种限定符,他们决定查找依赖的方式

  • @Optional()
  • @Self()
  • @SkipSelf()
  • @Host()

@Optional()

这个限定符比较简单,表示遇到 NullInjector 不报错,只是把依赖设置为可选的(空值)

@Self()

self 限定符,表示只在自己这里找依赖,找不到就是 NullInjector。

在 ModuleInjector 中,它表示的是当前 ModuleInjector 实例,在 ElementInjector 中同理。

因此在 Service 中的依赖,@Self()会限制在当前的 ModuleInjector 进行查找,而在组件中的依赖,则会限制在当前组件的 ElementInjector 中进行查找。

@SkipSelf()

正好与 self 相反,表示从父 Injector 开始进行查找,工作原理是一致的

@Host()

在 ModuleInjector 中,宿主就是自己的 ModuleInjector。

在 ElementInjector 中 组件和组件中的指令,公用一个宿主 ElementInjector,这个 ElementInjector 是由组件创建的。

默认情况组件的上一个宿主,是根 ElementInjector,也就是最外层提供了 ElementInjector 的组件。

但是如果这个组件在投影中的时候,它的宿主,会变成提供投影的组件的 ElementInjector。

在只使用@Host()时,我们得到的结果与@Self 是类似的,但是我们在组合@Host()与@SkipSelf()时,结果就要遵循上边的规则。

这里我们提供了一个 Stackblitz 示例 方便大家直观的观察在组件嵌套与组件投影情况下限定符的效果。

依赖注入的应用

有了依赖注入,我们可以在通过提供新的 provide 的方式,去更换某些已有的实现,最常见的应用就是我们的 RouterRueseStartegy,HttpInterceptor 等。

同时在应用过程中,也会带来一些新的问题,下面我们来探讨下可能会遇到的问题。

依赖注入的查找方式带来的坑

上边我们说过,依赖注入会逐层递归查找,如果找到了 provider,会优先返回缓存,再考虑构造新的实例。因此在实际应用中,会出现一些我们在子模块提供了新的 provide,但是却不生效的问题,例如之前的 HTTP_INTERCEPTORS。因此我们需要为子 Injector 重新提供 providers,以避免它从父级 Injector 直接取缓存。

今天的文章到这里,后续的文章会写一下 Injector 的高级用法,敬请期待。