Angular DI - 了解Ivy R3Injector

98 阅读6分钟

Angular的一个重要特性是依赖注入(Dependency Injection, 简称DI)。依赖注入通常被用来解耦模块间的依赖,但是,Angular使用它解决组件间的通信问题,这点与React的Context很相似。我们来了解下它的内部结构。

模型图

结合自己的理解,简单画了一张模型图

Angular18 Guess.jpg

上图有两颗树组成,一颗是红色区域的组件树(可能Angular组件树不完全长这样,这里假设便于说明DI),另一颗是非红色区域的Injector树,它就是DI的核心。

Injector树由Injector节点组成,Injector有多个变种,分别是NullInjectorPlatformInjectorRootInjectorModuleInjectorElementInjector。他们之间相互形成父子关系,子Injector持有父Injector的引用。当Injectable无法在子Injector找到时,会通过父引用向上查找(比如ElementInjector -> ModuleInjector -> RootInjector->...),直到找到目标Injectable或者到达NullInjector,抛出Null异常。另外,我们可以使用InjectFlags来控制向上查找的过程,比如使用SkipSelf来跳过Injector查找链上的某个Injector.

NOTE: 不同种类的Injector,比如PlatformInjectorRootInjectorElementInjector等,他们功能上并没有两样(都具有查找Injectable,和向上找到父Injector的能力),但他们在树中所处的位置和名称不同。

在Injector树中的位置越高,覆盖组件树中的节点就越多,也就是说被更多组件节点共享。Injector树与组件树是同步创建的,层级上有对应关系。名称使得我们能够在使用@Injectable装饰器时,设定provideIn等于root/platform来指定Injectable放到哪个Injector.

Injector实际上是一个容器,用来存放Injectable(Angular内部叫作Injectable),在应用中我们一般叫Service,我们简单看下Injectable与Injector两者的关系。比如,图中的组件节点c,假设它被C组件创建,该组件的@Component装饰器的providers设置了Provider类型数组值,所以Angular创建了一个ElementInjector A,把数组中的每个Provider转成Injectable存入ElementInjector A中。

现在,假设C组件的构造函数声明依赖Injectable N, Angular首先会从ElementInjector A查找 Injectable N。因为Injectable N就在当前Injector下,直接获取Injectable N,并作为参数调用构造函数生成组件实例(就是依赖注入)。

分析依赖注入(DI)

Angular依赖注入分成三块内容

  • Injectable的创建
  • Injectable存入Injector
  • 在Injector树上检索Injectable

Injectable的创建

任意一个Injectable需要包含三个信息,它们是

  1. Injectable应该存到哪个Injector中
  2. Injectable在Injector中使用哪个token来存储
  3. 如何创建Injectable的实例

Angular提供了Provider类型, @Injectable()两种方式来定义Injectable,我们看下他们是如何回答上述三个问题的。

@Injectable()

@Injectable用来装饰Service Class,这个Service Class即Injectable。装饰器提供的meta data会被存储在Class的静态属性eprov上(该属性由编译器生成),其中的provideIn值告诉Angular应当把Class(Injectable)存到哪个Injector。Class实例的创建也会被编译成静态方法ɵfac,这个工厂方法回答了如何创建Injectable实例。另外,Injector中会存储多个Injectable,所以需要token来标识(Injector内部使用Map数据结构来存储Injectable),这里Angular使用Class作为token存储。

graph TD
id1["makeDecorator('Injectable')"] --> getCompilerFacade --> compileInjectable --> id2["Object.defineProperty(Class, ɵprov)"] --> id3["Object.defineProperty(Class, ɵfac)"]

@Injectable装饰器的执行流程如上图,调用makeDecorator,创建Injectable装饰器方法并返回。在装饰器方法内部,通过getCompilerFacade获得JIT编译器实例,然后调用它的compileInjectable方法,该方法使用defineProperty把装饰器的meta data值赋给Class的ɵprov静态属性,同样把创建Class实例的工厂方法赋给静态ɵfac属性。

Provider

然后是Provider,它的定义形式例如{ provide: Logger, useClass: BetterLogger }。其中provide属性值指定了存储token。Angular提供了多种Provider类型,有useClass, useFactory, useExisting等,这些属性值实际在Angular内部会被转换成工厂函数factory,用来创建Injectable实例。由于Provider会配置在@NgModule@Component装饰器中,等于直接指明了Injectable存储在哪个ModuleInjector或者ElementInjector中。

--- 
title: Provider 转成 Factory方法
---
graph TD
A["providerToFactory(provider)"] --> B{Is it factory Provider?}
B -->|Yes| C["factory = () => provider.useFactory(...injectArgs(provider.deps))"]
B ---->|No| D{Is it Existing Provider}
D -->|Yes| E["factory = () => ɵɵinject(resolveForwardRef(provider.useExisting)"]
D -->|No| F[check other provider types...]
E -->G[return factory]
F -->G

Injectable存入Injector

无论Provider还是@Injectable()定义的Injectable,我们都能获取到factory方法。Angular会使用makeRecord方法把factory方法包装成Record对象,Record对象包含三个属性,factory、value、multi,数据结构如下

classDiagram 
class Record 
Record : Function factory
Record : any value
Record : Array multi

factory方法会被存到Record的factory属性上,value用来保存factory方法执行返回的Injectable实例。当Injectable被多次注入时,factory方法只有首次会被执行,创建Injectable实例并保存到value。后面都会直接取value的值,这就实现了单例模式。

下面Angular会把创建的Record对象,以Injectable定义中设置的token作为key,存储到Injector的records属性中,它是一个Map类型。Injector的数据结构如下

classDiagram 
class Injector{
    Map~ProviderToken,Record~ records
    constructor(providers: Array~Provider~,...)
    Fucntion processProvider
    Function providerToFactory
    Function makeRecord
}

简述下Injectable存入Injector的流程。Injector的构造函数拿到provides参数,遍历provides对每一项调用processProvider方法。processProvider方法内部,使用providerToFactory方法完成provider到factory方法的转换(参考上面Injectable创建的Provider部分),然后makeRecord接受factory方法参数,创建一个Record记录record,最后调用this.records.set(token, record)把record保存到Injector中。

--- 
title: Injectable存入Injector流程图说明
---
graph TD
A[Injector接受provides参数] --> B[循环遍历provides]
--> C[每个provide转成factory] --> D[factory转成record] --> E[record保存到Injector的属性records中]

在Injector树上检索Injectable

Injector上还有一个get方法,它根据token和flags参数,在Injector树上找到目的Injectable并返回。我们在Injector类图上,补充检索Injectable相关的方法定义。

classDiagram 
class Injector{
    Map~ProviderToken,Record~ records
    constructor(providers: Array~Provider~, readonly parent: Injector, readonly scopes: Set~InjectorScope~)
    Fucntion processProvider
    Function providerToFactory
    Function makeRecord
    Function get(token: ProviderToken~T~, flags: InjectFlags)
}

你可能会问Injector树是如何创建的,目前我们能够从Injector的构造函数上看到一二。每次构造新的Injector都会传入父Injector的引用,它会被该子Injector作为属性保存下来。由于Injector的创建依赖组件树的创建,可以简单地把Injector节点理解成组件节点的一个延伸数据结构,(除去PlatformInjecotor, RootInjector等顶端Injector,特指ElementInjector),因此在构造组件树时会同步形成一棵Injector树。

下面我们来简单解释下get方法做的事情(为了简单,忽略flags),如下图

graph TD
A[根据token检索records] --> B{"有目标Injectable(record)吗?"}
B -->|No| C{Injectable是否属于当前Injector?}
B -->|Yes| E
C -->|Yes| D["创建record保存到records"]
D -->E["factory创建Injectable实例并返回"]
C ---->|No| F["调用父Injector get方法查找"]
F --> A

get方法总共做了三步操作,分别是

  • 从Injector的records记录里查找Injectable。
  • 创建被@Injectable()装饰的Injectable。
  • 从父Injector上查找Injectable。
从Injector的records记录里查找Injectable

当前Injector调用this.records.get(token)查找目标Injectable(这里实际是从Injectable转成的Record,参考Provider转成Record的流程,可以认为两者是等价物)。如果找到,直接调用record.factory方法创建Injectable实例赋值给record.value并返回,当然如果record.value已经有值,直接返回无需调用factory,就像前面解释的单例模式

创建被@Injectable()装饰的Injectable

在records里找不到的情况下,待查找的Injectable可能是@Injectable()装饰的Class,参考前文@Injectable()部分。这时候token就是这个Class, 从Class的静态属性ɵprov上拿到provideIn值,与当前Injector的scopes比较,检查Injectable是否属于当前Injector。如果是,则取得ɵfac作为factory方法创建record并保存到records中。同时调用factory方法创建Injectable实例并返回。

从父Injector上查找Injectable

最后要是上面两种情况都不是,则调用this.parent.get(token)开始从父Injector上检索,在父Injector上执行相同的查找步骤。直到找到为止,或者到达顶端NullInjector抛出Null异常。

总结

Angular的依赖注入系统与React,Solid的context系统本质上是一样的,都是组件树或者类组件树上的一个延伸数据结构。只是他们对外提供的接口方式不同,Angular是基于类的工厂模式+依赖声明。React和Solid是基于函数式的hook。下次我们来看下Solid Context的设计。