Angular 17+ 高级教程 – Component 组件 の Dependency Injection & NodeInjector

前言

在 Dependency Injection 依赖注入 文章中,我们学习了 50% 的 Angular DI 知识,由于当时还不具备组件知识,所以我们无法完成另外 50% 的学习。

经过了几篇组件教程后,现在我们已经具备了基础的组件知识,那这一篇我们便来完成 Angular DI 所有内容吧。

 

主要参考

Angular in Depth – A Deep Dive into @Injectable and providedIn in Ivy

被删 – Angular冷知识--布隆过滤器

 

R3Injector, NullInjector, NodeInjector

这世界上不只有 R3Injector。

Dependency Injection 依赖注入 文章中,全篇我们讲的都是 R3Injector,但它只是 Angular 里其中一种 Injector。

NullInjector

NullInjector 和 R3Injector 不同,它没有 records 属性,它的 get 只负责 throw error 而已,只要有人调用 get 方法,就报错。

当 R3Injector.get 没有找到 provider 的时候也会报错。

const injector = Injector.create({
  providers: [],
});
console.log(injector.get(ServiceA));

仔细看会发现报错的来源是 NullInjector 而不非 R3Injector。原因是 R3Injector 的 parent 就是 NullInjector。

在 Injector.create 创建 R3Injector 时,如果没有指定 parent Injector 那么默认会使用 NullInjector 作为其 parent injector。

NodeInjector

NodeInjector 博大精深,比 R3Injector 复杂多了,它也是本篇的主角。

这里我提几个点,让大家有个画面先:

  1. NodeInjector 和 R3Injector 都是 Injector,但它俩内部的原理相差十万八千里。

  2. NodeInjector 里面没有 records 这个概念,它的查找逻辑和 R3Injector 是完全不一样的。

  3. NodeInjector 的 Node 指的就是 DOM 节点,所以它是围绕着 DOM 的注入器。R3Injector 你可以用传统的 DI(比如 ASP.NET Core)概念去理解它,

    但是 NodeInjector 则不可以。

  4. NodeInjector 一定是搭配组件 (Node) 使用的,我们不能像 R3Injector 那样脱离组件也能使用。

 

Injector Tree in Angular Project

Dependency Injection 依赖注入 文章中有提到,在真实项目中,Angular 会替我们创建 Injector,我们不需要自己 Injector.create。

那整个项目就只有一个 Injector 吗?

不是的,Angular 会创建多层级 Injector,也就是 hierarchical (parent child) 概念。

我们来看一张图

从最上层的 Null Injector 到底层级 Child Injector。我们一个一个看。

Null Injector

就是 NullInjector,介绍过了,负责 throw error。

Platform Injector

Platform Injector 是一个跨 Application 共享的 Injector。

什么叫跨 Application? 

一个 Angular 网站,其实可以有多个 Application。

比如

index.html

main.ts

app.component.ts 和 app2.component.ts

跨 Application Injector (Platform Injector) 可以让 AppComponent 和 App2Component inject 到同一个 Service 对象(单列模式)。

appConfig 和 app2Config 提供的 Provider 是给单一 Application 的,要想提供 Provider 给跨 Application 的 Platform Injector 有 2 种方法:

  1. providedIn: 'platform'

    @Injectable({
      providedIn: 'platform',
    })
    export class ServiceA {}

    使用 Injectable 或 InjectionToken 的 providedIn: 'platform'。

  2. platformBrowser

    在 main.ts 执行 platformBrowser 函数,并传入 providers。

    注:参数类型是比较紧的 Array<StaticProvider>,而不是像 appConfig.providers 那样宽松的 Array<Provider | EnvironmentProviders>。

Platform Injector 是一种 R3Injector

Platform Injector 其实也是用 Injector.create 创建出来的,它的类型依然是 R3Injector。只是它多了一个 Provider INJECTOR_SCOPE = 'platform' 而已。

Root Injector (a.k.a Application Injector)

Root injector 比 Platform Injector 再低一级 (Root Injector 的 parent 是 Platform Injector),它属于 Application Level。

同一个例子,一个 Project 有 2 个 Application,AppComponent 和 App2Component,

在 Root / Application Level 下,它两 inject 的 ServiceA 将会是不同的实例,没有跨 Application 共享的概念。

我们通过 appConfig.providers 提供的 Provider 便是给 Root Injector 的。

通过 providedIn 也可以提供给 Root Injector

@Injectable({
  providedIn: 'root',
})
export class ServiceA {}

Root Injector 也是 R3Injector,它的 INJECTOR_SCOPE = 'root'。

App Standalone Injector

当 App 是 Standalone 组件 (v15 之后默认创建的 App 都是 standalone 组件),同时它有 import NgModule,那就会有多一个 App Standalone Injector。

比如说 App 组件 import 了 CommonModule,这样就会有多一个 App Standalone Injector 出来,如果完全没有 import 任何 NgModule 就不会有这个 App Standalone Injector。

虽然 NgModule 我还没有教,但组件 import NgModule 是很常见的 (比如 CommonModule, RouterModule 等等),所以在整体结构上我把它展示出来,让大家知道有它这么一个存在。

至于这个 App Standalone Injector 具体有什么特别之处,我也没有深入理解,只知道它也是一个 R3Injector,它的 parent 是 Root Injector。

注:往后的教程,在结构上我默认会放 App Standalone Injector 出来,但大家可以把它和 Root Injector 两个当成一个 Injector 看待,不用太在意它和 Root Injector 之间的区别。

ChainedInjector

ChainedInjector 的类型不是 NodeInjector 也不是 R3Injector,它和 NullInjector 一样,都是直接 implements Injector 而已。

ChainedInjector 顾名思义哦,它是作为 NodeInjector 链接到 R3Injector 的链子。当我们在组件内 inject 时,会从 NodeInjector 开始找,

比如 Child2 NodeInjector > App NodeInjector > ChainedInjector 

ChainedInjector 里面可以收藏一个 Injector,上面的例子中,它收藏的是 NullInjector(注:在 Dynamic Component 的情况,通常不会是 NullInjector,关于 Dynamic Component 以后才会教)

这个收藏的 Injector 也找不到的话,它会继续往上找 App Standalone Injector > Root Injector > Platform Injector > NullInjector。

App NodeInjector & Child NodeInjector

App & Child 都是 NodeInjector,Angular 所有组件用的 Injector 都是 NodeInjector。

 

NodeInjector 简单介绍

NodeInjector 和 R3Injector 都是 injector,都是用来注入的,但是它俩的内部工作机制是完全不一样的。

为了由浅入深,我们先用一个简单(但不正确)的方式去理解和使用 NodeInjector。

  1. 每一个组件都有属于自己的 NodeInjector。

  2. 每一个组件都可以提供自己的 providers 给它的 NodeInjector。

  3. 组件有父子关系,同样的它们的 NodeInjector 也是父子关系。

Inject Global Service

@Injectable({ providedIn: 'any' })
class ServiceA {}

@Injectable({ providedIn: 'platform' })
class ServiceB {}

@Injectable({ providedIn: 'root' })
class ServiceC {}

ServiceA, B, C 分别提供给 Any Injector、Platform Injector 和 Root Injector。

在任何组件内都可以 inject 到这些 services。

export class AppComponent {
  constructor() {
    const serviceA = inject(ServiceA);
    const serviceB = inject(ServiceB);
    const serviceC = inject(ServiceC);
  }
}

因为组件的 NodeInjector 也有 parent child 概念,它会一直往上查找,直到 Root Injecotor > Platform Injector > Null Injector 报错。

Component Level Provider

到目前为止,我们学过的 Provider 只能提供给 any、platform 和 root injector。

for 各个组件的 NodeInjector,我们可以通过 @Component decorator 去提供 providers。

我们来看个例子。

下面有 4 个组件,AppComponent、AComponent、AaComponent 和 BComponent。

 

我们在 A 组件 provide ServiceA。

@Component({
  selector: 'app-a',
  standalone: true,
  templateUrl: './a.component.html',
  styleUrl: './a.component.scss',
  imports: [AaComponent],
  providers: [ServiceA], // 提供 ServiceA 给 AComponent NodeInjector,providers 的类型和 appConfig.providers 大同小异 (a.k.a 不完全一致)
})
export class AComponent {}

A 组件和旗下所有的后裔组件 (Aa 组件) 都可以 inject 到 ServiceA。

而它的 parent (App 组件) 和 sibling (B 组件) 则 inject 不到 ServiceA。(这个很好理解,Child Injector 可以 inject Parent Injector 的 providers,但反过来则不行)

 

深入理解 NodeInjector

上一 part 我们用了一种简单但错误的方式去理解 NodeInjector。

怎么说呢?看例子

假如我们在组件内 inject Injector,每一次都会得到不同的 NodeInjector 实例。

所以 Angular 并没有为每一个组件创建一个 NodeInjector,那样的理解是错误的。

我们来逛一逛源码,揭秘一下。 

Angular bootstrapApplication 源码逛一逛

要理解 NodeInjector,我们需要先理解 TView 和 LView,它俩我在 Change Detection 文章中有教过了(请先看完才继续这篇),但那一篇主要是围绕着 Change Detection 相关的部分做讲解。

这一篇,我们同样再讲一篇 TView 和 LView,但这一次围绕的是和 DI 相关的部分做讲解。

首先,创建一个简单的项目

@Component({
  selector: 'app-root',
  standalone: true,
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
  imports: [CommonModule],
  providers: [ServiceA],
})
export class AppComponent {
  constructor() {
    const serviceA = inject(ServiceA);
  }
}

一个 App 组件,它提供了一个 ServiceA Provider,组件在 constructor 阶段使用 inject 函数注入 ServiceA。

我们要搞清楚的是,上面这一整个注入过程是如何发生的,它的原理和机制是怎样的。(注:下面不会一行行代码解释,只会解释重要相关的代码)

compilation 阶段

App 组件被 compile 之后长这样

app.component.js

main.js 长这样

bootstrapApplication 函数的源码在 application_ref.ts,看注释理解。

createProvidersConfig 函数

这些 built-in 的 providers 我们现在不需要懂,以后有机会再学呗。

internalCreateApplication 函数

createOrReusePlatformInjector 函数

createPlatformInjector 函数

回到 internalCreateApplication 函数

EnvironmentNgModuleRefAdapter 类

到这里,Platform Injector 和 Root Injector 都做出来了。

回到 internalCreateApplication 函数

ApplicationRef 类

回到 internalCreateApplication 函数

bootstrap 函数

create 函数的源码在 component_ref.ts

ChainedInjector 类

回到 create 函数

继续

TView 和 LView 里存放了哪些资料,我们以前在 Change Detection 文章里讲解过。

但那时主要讲的是位置 0 – 24 存放的资料。这里我们继续讲 24 位以后,它又存放了哪些资料。

TView.data 是 array,LView 也是 array,它俩是相互对应的。

0 – 24 位置之前讲过,我就不再讲了。

25 – ? 位置存放的是 nodes 资料,为什么结尾是问号,因为这个要依据它 node 的数量决定。

例子说明:

Root TView 里只有一个 App 组件,

所以 Root TView.data[25] 存放的是 App TNode。(注:我这里是提前讲解结构,目前这个环节 App TNode 其实还没有被创建。)

而 Root LView[25] 存放的是 App LView。

再一个例子

App Template 有 2 个 nodes。

一个 h1,一个 text

所以 App TView[25] 是 h1 的 TNode,TVIew[26] 是 text 的 TNode

上图就是 App TVIew.data。

25 是一个 TNode,type = 2,26 则是 type = 1。

这个 TNode.type 是这样看的

prefix 0b 的意思是它是二进制,去掉 0b,Text = 1,Element = 10(也就是十进制的 2)

白话文就是 TNode.type = 1 表示这个 TNode 是一个 Text,type = 2 表示这个 TNode 是一个 Element。(注:组件也算是 Element)

下图是 App LView

25 放的是 h1 HTMLElement 实例(它就是 DOM 节点,等同于我们用 document.querySelector 拿到的是一样的),26 是 Text 实例。

好,那放完 nodes 资料之后,还有什么资料是存放在 TVIew 和 LView 的呢?

答案是本篇的主角 NodeInjector 的资料。

继续看例子,AppComponent 提供了一个 ServiceA Provider。

AppComponent 的 providers 会被记入到它的 parent TView 和 LView 里,也就是 Root TView 和 LView。(注:不是记入在 App LView 哦,而是记入在它的 parent -- Root LView)

下面这个是 Root LView(注:TView 和 LView 虽然有差别,但差别不是很大,这里我先讲 LView 大家有个画面就好,细节下面看源码时还会再讲)

0 – 24 是 Angular 固定的资料,25 – ? 是 nodes 资料,Root LView 只有 1 个 App 组件,所以 25 是 App LView。

那 26 开始就是 NodeInjector 资料。开头是 8 个零再配一个 -1,然后是 ServiceA 和 App 组件实例。

前面 8 个位置是一个 Bloom Filter(布隆过滤器)编号,第 9 位是 parent NodeInjector 的坐标。具体这些资料如何被使用,我们先不管。

我们把 26 – 36 想象成是一个 Injector,它包含 ServiceA 和 AppComponent 这两个 Provider。

为什么 AppComponent 也会在 providers 中呢?

因为 AppComponent 也是 DI 的一部分,实例化 App 组件就是通过 Injector 来实现的。

它类似于下图

注:当然实际上代码不是上面这样,我只是做个比喻,能理解就可以了。

好,到这里我们已经知道了组件的 Injector 和 providers 资料最终是被记入到了 TView 和 LView 里头。

这点和 R3Injector 的机制就完全不同的。

R3Injector 是一个对象,providers 资料记入在 records 属性,parent 关系记入在 parent 属性,所有查找需要的资料都记入在对象里面。

NodeInjector 也是对象,但它没有 records 属性,也没有 parent 属性,所有 providers 和 parent injector 资料被记入在 TVIew 和 LView 里面。

所以当组件在做 inject 时,它查找的过程是通过 LView 和 TView 去找 Provider 的。

我们甚至可以认为 NodeInjector 其实只是一个不太重要的代理人,因为资料根本不再它手中。

这也是为什么上面我们在组件 constructor 使用 inject(Injector) 时,它每一次都返回不同实例也无所谓。

小结论:

在组件内 inject,它的查找过程是去 TVIew 和 LView 找,然后一直沿着 LView Tree 往上找,一直找到 Root LView,

然后去到 ChainedInjector 继续找 Root > Platform > Null Injector 结束。

好,相信大家应该有点画面了,那我们继续逛源码看完这个过程。

enterView 函数的源码在 state.ts

回到 create 函数

这个 Component Definition 指的是 app.component.js > AppComponent 类的静态属性 ɵcmp

里面就是一堆的 metadata,用于创建组件的资料。

回到 create 函数

createRootComponentView 函数

到这里,TVIew,LView,TNode 结构已经有了一些:

Root TView、Root LView、App TNode、App TView、App LView。

这时 AppComponent 类还没有被实例化哦,只是 Logical View 结构出来了而已。

回到 create 函数

createRootComponent 函数

initializeDirectives 函数的源码在 shared.ts

getOrCreateNodeInjectorForNode 函数的源码在 di.ts

insertBloom 函数

回到 getOrCreateNodeInjectorForNode 函数

回到 initializeDirectives 函数

diPublicInInjector 函数的源码在 di.ts

bloomAdd 函数

回到 initializeDirectives 函数

我们先来看看 AppComponent Definition 的 providersResolver 是啥,它从哪里来。

app.component.js

ɵɵProvidersFeature 函数的源码在 providers_feature.ts

回到 app.component.js

ɵɵdefineComponent 函数的源码在 definition.ts

initFeatures 函数

回到 initializeDirectives 函数

providersResolver 函数的源码在 di_setup.ts

resolveProvider 函数

继续

继续

回到 initializeDirectives 函数

继续

configureViewWithDirective 函数

getFactoryDef 函数的源码在 definition_factory.ts

这个 NG_FACTORY_DEF 是啥?它从哪里来的?

首先 @Component decorator 的源码在 directives.ts

compileComponent 函数

addDirectiveFactoryDef 函数

app.component.js

回到 configureViewWithDirective 函数

继续

到这里,我们的 AppComponent 都还没有实例化哦。

要实例化 AppComponent 就要先搞出一个 Injector,因为组件 constructor 阶段会调用 inject 函数做注入。

而这个 inject 函数只可以在 injection context 下才能执行。什么意思?简单说就是要先 set 一个全局变量 current injector 才可以调用 inject 函数。

我们继续逛源码,看看它是如何去实例化 AppComponent,如何 set current injector。

回到 createRootComponent 函数

getNodeInjectable 函数源码在 di.ts

isFactory 函数源码在 injector.ts

回到 getNodeInjectable 函数

忘了 runInInjectionContext 函数的,请温习这篇

factory 是 NodeInjectorFactory,那 factory.injectImpl 长啥样呢?

我们回看 configureViewWithDirective 函数中,AppComponent NodeInjectorFactory 的创建过程。

class NodeInjectorFactory 源码在 injector.ts

回到 getNodeInjectable 函数

继续

factory.factory 长啥样?

回顾 class NodeInjectorFactory 

回顾 configureViewWithDirective 函数

小总结一下,

到这里,已经 set 了全局变量 inject implementation 函数,这样组件内就可以使用 inject 函数了。

另外 NodeInjector.factory() 便是 AppComponent.ɵfac(),其内部便是 new AppComponent()

好,inject 函数我们下一个 part 在单独讲解,因为查找 Provider 的过程还会涉及到 Bloom Filter,异常的复杂,我们先把这个环节的大流程走完。

回到 createRootComponent 函数

回到 ComponentFactory.create 函数

App 组件实例化后,下一个阶段就是渲染它的 Template。

renderView 的源码我就不展开了,因为它和这篇的主题 DI 没有那么深的关系了,我大概讲一些就好。

renderView 函数内部会执行 App template 方法 by create mode。

template 方法里有 ɵɵelementStart,ɵɵText,这些就会去创建 DOM nodes。

如果有子组件,那它也会经历一遍类似(注:过程不完全一样,但这里我们不需要在意)创建 App 组件那样的过程,

比如:创建 TVIew,LView > 把 providers 资料放入 parent LView > 实例化子组件 > 然后又继续下一个 renderView 函数。整个渲染过程会一直渲染到所有后裔组件。

好,到目前为止所有的组件都被实例化了,LView Tree 也成型了。

但是渲染还没有完成,因为现在只是做了 create 的部分,Angular 渲染分 2 个部分,一个 create 一个 update,这个在之前的 Change Detection 文章中 有提到过。

我们继续看完它。

回到 ApplicationRef.bootstrap 函数

_loadComponent 方法

上面我们有提到,到目前为止,我们只完成了 create 部分的渲染,还有 update 没有完成。

这个 _loadComponent 就是执行 update 渲染过程的,在 Change Detection 文章中,我们学过 tick 就是从 Root LView 开始往下遍历后裔 LView 并且 refreshView。

这样所有 binding data 就会渲染进最终的 DOM 了。.

至此 Angular bootstrapApplication 结束,整个过程:

  1. 创建 Platform Injector
  2. 创建 Root Injector
  3. 创建 Root View Injector (ChainedInjector)
  4. 创建 Host RNode <app-root>
  5. 创建 Root TView
  6. 创建 Root LView
  7. 创建 App TNode
  8. 创建 App TView
  9. 创建 App LView
  10. 创建 App Injector 资料, 写进 Root T/LView
  11. 弄 App.providers 资料写进 App Injector 资料
  12. 弄 class App as Provider 资料写进 App Injector 资料
  13. 通过 injection 手法实例化 App 组件
  14. 渲染 App 模板 (create mode) 以及模板里的后裔组件。
  15. 添加 App hostView 到 ApplicationRef
  16. ApplicationRef.tick 渲染模板 (update mode)

至关重要的点

我觉得有几个点是蛮重要的,理解了这些就不会再有困惑。

  1. 不要把 NodeInjector 比作 R3Injector。

    R3Injector 对象是整个 DI 的核心。它保存 providers 也保存 parent injector 资料。

    而 NodeInjector 只是一个不起眼的小对象,它没有任何 providers 的资料,也没有 parent injector 的资料,正真存放 provider 和 parent injector 资料的地方是在 TView 和 LView 里。

    所以在组件内 NodeInjector 也好,inject 函数也好都只是一个代理,它们是去 TView 和 LView 里面找资料而已。

  2. 每个组件都有一份 NodeInjector 资料,不是 NodeInjector 对象哦,是资料。这些资料被存放在这个组件的 parent TView 和 LView 里。

 

题外话,关于 componentRef.hostView

虽然这个知识跟 DI 没有关系,但上面既然已经提到了,我们就顺便说一说把,也好为下一篇 Dynamic Component 做准备。

App 组件的 hostView 里面装着 Root LView。目前我们有三个词有点傻傻分不清楚。

  1. hostView
  2. Root LView
  3. Parent LView

我们来理清楚一下。

首先要知道,创建组件有 2 种方式。

第一种是 componentFactory.create,这也是创建 App 组件的方式。它被称为动态创建组件 dynamic create component。

第二种是在渲染模板的时候(上面提到的 renderView 函数)遇到子组件时创建它。这两种创建组件的过程几乎是一样的,只有一个地方不同。

那就是第一种方式,在创建组件的时候,它会生成 2 个 LView,而第二种方式只会生成一个 LView(也就是组件本身的 LView)。

那为什么会这样呢?

首先,每个组件是一定要有一个 Parent LView 的,因为组件 providers 不是记入在组件本身的 LView,而是记入在它的 Parent LView。

而第二种方式是发生在渲染模板阶段,这也意味着此时肯定已经有一个 LView 存在,不然怎么渲染模板,那这个已存在的 LView 自然可以成为组件的 Parent LView。

但第一种方式却不同,它发生的时候是完全没有 LView 存在的,于是一定要先创建一个 LView,然后把这个 LView 作为组件的 Parent LView。

好,那我们来整理一下。

componentFactory.create 用于动态创建组件,它会返回一个 ComponentRef (Ref 是 reference 的意思,简单说就是一个对象里面包裹了组件信息),

ComponentRef 里有一个 hostVIew,hostView 的类型是 ViewRef (又一个 Ref,顾名思义就是个对象,里面包裹了 View 信息)

hostView._lView 便是在创建过程中生成的两个 LView 中的第一个,我们可以称它为 Host LView,它也就是这个组件的 Parent LView。所以 hostView 包裹了 Host LView 而 Host LView === Parent LView。

如果说这个组件是 App 组件,那这个 Host LView 同时也是整个 LView Tree 的 Root LView。

所以在 App 组件的视角下 Host LView === Parent LView === Root LView。

如果不是 App 组件,那就不一定是 Root LView,如果不是动态组件就没有 Host LView。

总之,记住两点:

  1. 只有动态创建的组件才有 hostView 的概念。template 方法过程中创建的组件没有这个概念。

  2. 动态创建组件会同时生成 2 个 LView。一个是组件 LView,一个是组件 Host / Parent LView。

 

Bloom Filter 详解

上面我们逛源码时,我刻意跳过了 Bloom Filter 算法和 inject 函数,这里补上 Bloom Filter。

Bloom Filter 是什么?

推荐看这篇解答:被删 – Angular冷知识--布隆过滤器

我用视频中的例子稍微解释一下

假设有个弱密码的数据库表,当用户要设置密码时,程序会先查看这个表,如果用户设置的是弱密码,那就不允许。

程序通过网路链接服务器查找数据库,这个过程有两个点值得注意

  1. 需要通过网路,用户会感觉慢

  2. 会增加服务器的负担

这时就可以利用 Bloom Filter 做优化。它的原理是这样的。

假设我们的弱密码有这些 ['a', 'b', 'c', 'd', 'e']

首先拿 'a' 进行 bloom 算法得出一个编号(比如:0 0 0 0 0 0 0 1,这个编号的长度(有多少个 0)depend on bloom 算法的设置,通常总数据量越长它就越长,但也不会有多长啦,有点像 sha256 的概念)

再拿第二个值 'b' 进行 bloom 算法得出另一个编号。

接着把这 2 个编号累加在一起。依此类推直到全部弱密码都处理完,得到一个总编号。

bloom 算法 + 累加最神奇的地方是它的总编号不会越来越长,不管数据有多少,长度始终控制在一个非常小的范围,就像 sha256 那样。

当想要查找的时候,首先把要查找的值进行 bloom 算法得出一个编号,然后拿这个编号去匹配总编号。

这个匹配会得出 2 种结论。

  1. 这个值,绝对没有在集合中

  2. 这个值,有可能在集合中

绝对没有就表示用户输入的不是弱密码,可以通过。

有可能在集合中,那程序就需要再去服务器查找,做最后的确认。

为什么一个是绝对,一个只是有可能呢?

因为算法为了维持大小,牺牲掉了准确性。所以总编号才能一直保存很小的 length。

总结

Bloom Filter 是个优化方案,它可以把一个大集合压缩成一个小编号,通过比对小编号,我们可以快速的知道一个值是否绝对没有在集合中。

但如果它有可能在集合中,那我们依然需要去查询集合做确认。

所以这个优化方案并不是那种绝对的优化,它只是在部分情况下可以得到优化。

Angular 跟 Bloom Filter 的关系

参考:Angular 作者 MIŠKO HEVERY – Bloom Filters Can Speed Up Your Code

在上面逛源码时,我们看到 Angular 在处理 NodeInjector 资料时,会搞一个 bloom 算法得出一个编号,初始值是 8 个零。

它的目的就是要搞 Bloom Filter 做性能优化。想象一下,所有的组件 providers 就是那个大集合,每一个都会被 bloom 算法累加到一个总编号。

当我们想 inject(Provider) 时,Angular 会先去总编号比对一下,如果绝对不在集合里,那 Angular 会跳过所有组件的 providers,直接去 Root LView Injector (ChainedInjector) 里找。

虽然我觉得这个性能优化有点小题大做,但这确实是 Angular 团队或者说 MIŠKO HEVERY 的一贯风格啦。

Angular Bloom 算法

在上面源码中,有一个 bloomAdd 函数,它就是负责 Angular Bloom 算法的。我们看看它具体干了什么。

第一点,每一个组件的 providers,不管是 class 还是 InjectionToken 都会被设置一个静态属性 "__NG_ELEMENT_ID__"。它是一个累加的 ID,从 0 开始一直往上加。

第二点,每一个 NodeInjector 都有一个编号,默认值是 8 个零。

假设,我们的 App 组件有 3 个 Service。

包括 App 组件本身,一共有 4 个 Provider。它们的 ID 分别是 0、1、2、3。

在算法还没有跑之前,App NodeInjector 的编号 是 8 个零。0 0 0 0 0 0 0 0。

下面这个就是它的 Bloom 算法

// 1. 假设 App 组件有 4 个 Provider(AppComponent、ServiceA、ServiceB、ServiceC)
//    id 从 0 开始累加
const ids = [0, 1, 2, 3];
// 2. App NodeInjector 的 Bloom 编号,初始值是 8 个零
const nodeInjectorBloomCode = [0, 0, 0, 0, 0, 0, 0, 0];

// 3. 把每一个 id 经过 bloom 算法,然后累加进 NodeInjector Bloom 编号
for (let id of ids)
{
  // 4. Bloom 的配置,这个是 Angular 自己定的
  const BLOOM_SIZE = 256;
  const BLOOM_MASK = BLOOM_SIZE - 1;
  const BLOOM_BUCKET_BITS = 5;

  // 5. 算法开始
  const bloomHash = id & BLOOM_MASK;
  const mask = 1 << bloomHash;
  // 6. 插入第几个位置
  const slot = bloomHash >> BLOOM_BUCKET_BITS;
  // 7. 插入 & 累加
  nodeInjectorBloomCode[slot] |= mask;
}

// 8. 最终 App NodeInjector 的编号
console.log('injector', nodeInjectorBloomCode); // [15, 0, 0, 0, 0, 0, 0, 0]

为了方便看,我把源码抽出来做了一些整理。

如果你想搞明白每一个细节,可以把 ids 设置成 0...256,然后把每一次 nodeInjectorBloomCode 打印出来看它的变化。

里头的二进制操作符,不熟悉的朋友也可以看这篇 C# and TypeScript – Enum Flags

好,我们来验证看最终结果是不是这样。

进入 Root TView.data[26-33] 就可以看到 App NodeInjector Bloom 编号了。

我们再来看一个带有 parent NodeInjector 的例子。

组件结构是这样,App 组件 > Parent 组件 > Child 组件。

Provider 的 ids

它们各个的 NodeInjectorBloomCode

这些是 TView.data 的记入,我们再看看 LView 的记入。LView 记入的是累加的 Parent Injector 的资料。

Parent 的 15 来自 App TView.data 的 15

Child 的 63 来自 App + Parent TView.data,也就是 15 | 48 = 63。

 

inject 函数查找过程

inject 的查找过程有点复杂,有点绕,我们先不看源码,看例子比较容易理解。

有 4 个不同的例子

例子一:Child 组件注入 Child Provider

第 1 步 

拿出 Service5 的 id(Service5['__NG_ELEMENT_ID__'])。id 是 7。

第 2 步 

拿 id 跑 Bloom 算法。得出编号 128 0 0 0 0 0 0 0。

第 3 步

有了 Provider 编号,接下来要找出 Child NodeInjector 的编号。

拿 current TNode(也就是 Child 组件的 TNode)。

TNode 属性 injectorIndex 表示 Child NodeInjector 在 Parent TView 的 index。(再次提醒,Child NodeInjector 资料是记入在 Parent TView 和 LView 里,而不是 Child 本身的 TVIew 和 LView)

第 4 步

有了 injector index 就去拿 injector 编号。

Child NodeInjector 的编号是 192。(注:因为这些例子 Provider 数量都很少,用不到后面的 7 个位,所以我省略掉了后面的 7 个零)

第 5 步

对比 Service5 id 的编号和 Child NodeInjector 的编号

const childNodeInjector = 192;
const service5 = 128;
const matched = (childNodeInjector & service5) > 0; // true

matched 表示 Provider 有可能在这个 NodeInjector 内。

第 6 步

找出 Provider index。

NodeInjector 有 8 个位置,紧跟着的便是这个 NodeInjector 的 providers。

从 36 开始一个一个找。

36 是 Child TNode,37 是 class Service5。index 37 就是我们要的。

第 7 步

到 Parent LView[37] 获取 NodeInjectorFactory

调用这个 factory 就可以获得 Service5 实例了。至此 inject 查找和实例化就完成了。

例子二:Child 组件注入 App 组件。

第 1 步

拿出 App 的 id(AppComponent['__NG_ELEMENT_ID__'])。id 是 0。

第 2 步

拿 id 跑 Bloom 算法。编号是 1 0 0 0 0 0 0 0。

第 3 步

和例子 1 一样,Child NodeInjector index = 28。

第 4 步

和例子 1 一样,Child NodeInjector 的编号是 192。

第 5 步

对比 App id 的编号和 Child NodeInjector 的编号

const childNodeInjector = 192;
const app = 1;
const matched = (childNodeInjector & app) > 0; // false

unmatched 表示 Provider 不在这个 NodeInjector 内。我们需要往 Parent NodeInjector 找。

第 6 步

用 Child NodeInjector index 到 LView[28] 拿 Injector 资料

第 28 位的 63 是 Parent NodeInjector 的累加编号。我们一样拿它来比对。

const parentAccumulateNodeInjector = 63;
const app = 1;
const matched = (parentAccumulateNodeInjector & app) > 0; // true

matched 表示 Provider 有可能在 Parent NodeInjector 里。

第 7 步

我们要找出 Parent NodeInjector 的 Index。上面那一张图是 Parent LView 中 Child NodeInjector 位置

第 36 位的 65564 是 Parent NodeInjector 的坐标。

const INJECTOR_INDEX_MASK = 0b111111111111111; // 相等于十进制 32767
const parentNodeInjectorIndex = 65564 & INJECTOR_INDEX_MASK; // 28

把坐标换算成 index。Parent NodeInjector Index = 28。

第 8 步

找 Parent NodeInjector。

当前 LView 的 28 位是 Child NodeInjector,这也就意味着 Parent NodeInjector 不可能在这个 LView 里,因为 28 位已经是 Child NodeInjector 了。那 Parent NodeInjector 肯定是在上一层的 LView(也就是 App LView)。

注意:一个 LView 里面是有可能出现超过 1 个 NodeInjector 的。Parent NodeInjector 也有可能和 Child NodeInjector 在同一个 LView 里。这个场景我下面会特别讲解。现阶段我们没有这个场景,暂时不需要太在意。

现在去上一层 LView,也就是 App LView。

然后去 App TView[28] 获取 Parent NodeInjector 编号

第 9 步

这边就一直重复第 5 到第 7 步,一直到 match 到为止。

拿 Parent NodeInjector 编号和 Provider 编号比对

const parentNodeInjector = 48;
const app = 1;
const matched = (parentNodeInjector & app) > 0; // false

一样是 unmatched。继续往上找

这个是 App LView 

const appAccumulateNodeInjector = 15;
const app = 1;
const matched = (appAccumulateNodeInjector & app) > 0; // true

matched 表示 Provider 可能在 App NodeInjector 里。

const INJECTOR_INDEX_MASK = 0b111111111111111; // 相等于十进制 32767
const appNodeInjectorIndex = 65562 & INJECTOR_INDEX_MASK; // 26

到 Root TView.data[26] 

const appNodeInjector = 15;
const app = 1;
const matched = (parentNodeInjector & app) > 0; // true

总算是 match 到了。所以 AppCompoent Provider 有可能在 App NodeInjector 里。

第 9 步

和例子 1 的第 6,第 7 步一样。

例子三:Child 组件注入 Root Injector Provider

我们把 Service5 换成 provide 给 Root Injector。

第 1 步

拿出 Service5 的 id(Service5['__NG_ELEMENT_ID__'])。id 是 undefined。

因为 Service5 不是组件的 providers,所以它自然就没有 __NG_ELEMENT_ID__。

第 2 步

直接通过 LView[9] 获取 Root LView Injector (ChainedInjector) 做查找,它会一路找到 Root > Platform > NullInjector。

例子四:Not in Ancestor NodeInjector

这个例子的组件结构和前几个例子不同。它有两条分支。

Parent1 有 providers: [Service1],Child2 尝试 inject(Service1)。

通过前面几个例子,相信大家对流程都蛮熟悉了,我这里划重点就好。

第 1 步

Service1 是 Parent1 组件的 Provider,所以 Service1 会有 id。

第 2 步

拿 Service1 id 比对 Child2 NodeInjector 结果是 unmatched。这表示 Service1 不在 Child2 NodeInjector 的 Provider 里。

第 3 步

拿 Service1 id 比对 Parent2 Accumulate NodeInjector 结果也是 unmatched。这表示 Service1 不在任何祖先 NodeInjector 的 Provider 里。

第 4 步

直接通过 LView[9] 获取 Root LView Injector (ChainedInjector) 做查找,它会一路找到 Root > Platform > NullInjector。

 

inject 函数源码逛一逛

查找流程已经讲的那么清楚了,其实没有必要再逛源码了,不过有始有终呗,我们就稍微逛一下就好。

组件内会调用 inject 函数

inject 函数源码在 injector_compatibility.ts

ɵɵinject 函数

getInjectImplementation 函数的源码在 inject_switch.ts

回到 ɵɵinject 函数,进入 injectInjectorOnly 函数的源码在 injector_compatibility.ts

回到 ɵɵinject 函数

我们之前就讲过很多次了,inject 函数只可以在 injection context 内被执行。

那怎样才算是在 injection context 呢?就是 inject 执行前,已经 set 好一个全局 Injector 或者 inject implementation 函数。

下面是一个 runInInjectionContext 例子

class ServiceA {}
const injector = Injector.create({
  providers: [ServiceA],
});
runInInjectionContext(injector, () => {
  const serviceA = inject(ServiceA);
});

runInInjectionContext 函数会先把参数 1 injector set 去全局 injector,然后才调用参数 2 的函数。所以参数 2 内就可以使用 inject 函数。

在 Angular bootstrapApplication 过程中有一个阶段是 getNodeInjectable 函数

它是在实例化 App 组件之前发生的,它把 全局的 inject implementation 函数 set 成了 ɵɵdirectiveInject 函数。

所以在 App 组件 constructor 里面,调用 inject 函数等同于调用了 ɵɵdirectiveInject 函数。

ɵɵdirectiveInject 函数源码在 di.ts

getOrCreateInjectable 函数源码在 di.ts

lookupTokenUsingNodeInjector 函数

bloomHashBitOrFactory 函数

回到 lookupTokenUsingNodeInjector 函数

for number >= 0 的情况就是上一 part <<inject 函数查找过程>> 的全部过程。这块我就不再深入源码了。

for function 的情况是以下 6 个特殊的 inject token。

export class AppComponent {
  constructor() {
    inject(ChangeDetectorRef);
    inject(ElementRef);
    inject(TemplateRef);
    inject(ViewContainerRef);
    inject(Renderer2);
    inject(Injector);
  }
}

ChangeDetectorRef  我们在 Change Detection 文章中学过了

ElementRef 是当前组件的 DOM Element 资料,属性 nativeElement 指向 HTMLElement,通过这个可以直接操作 DOM。

Renderer2 是 Angular 封装的 DOM 操作接口,如果我们有跑 SSR(Server-side Render),那在组件 constructor 阶段是不可以直接操作 DOM 的(会报错),必须通过 Renderer2 提供的接口间接的操作 DOM。

Injector 是 NodeInjector 咯,当我们要在某个组件方法里注入 Provider 时就需要在 constructor 阶段先把 NodeInjector 拿出来,因为组件方法里是不能直接调用 inject 函数的,因为它不是 injection context。

TemplateRef, ViewContainerRef, DestroyRef 以后会教。

这些 token 的 __NG_ELEMENT_ID__ 都不是 number,而是函数。

由于这些都不算是组件 providers,所以 Angular 需要特殊处理。

好,源码就逛到这里。最后附上一张美美的流程图。

 

当 NodeInjector 遇上 Content Projection の viewProviders and host

当 NodeInjector 遇上 Content Projection (a.k.a slot / transclude) 会有一些化学反应。这一 part 我们来详细了解一下。

如果你对 Content Projection 不熟悉的话,请先复习这篇 Component 组件 の Angular Component vs Shadow DOM (CSS Isolation & slot)

Content Projection 场景下的 NodeInjector 和 Logical View

例子

有 3 个组件:

App 组件、Parent 组件、Child 组件。

Child 组件被 transclude 到 Parent 组件里。

LView 和 NodeInjector Q & A

  1. 总共有几个 LView?

    4 个,Root LView、App LView、Parent LView、Child LView。

  2. 总共有几个 NodeInjector?

    3 个,App NodeInjector、Parent NodeInjector、Child NodeInjector。

  3. Child LView 的 parent LView 是谁?

    是 App LView!不是 Parent LView 哦。

    只要是 app.component.html 里的组件,它们的 parent LView 都是 App LView。

    注:Grandchild 组件只是我为了解释这一题特别加的。

  4. Child NodeInjector 的 parent Injector 是谁?

    是 Parent NodeInjector。不是 App NodeInjector 哦,这一点和上一题 LView 的逻辑不一样。

    下图是 App LView

viewProviders

继续提升难度,我们在 Parent 组件添加 Provider -- Service1。

问:Child 组件可以 inject(Service1) 吗?

答:可以,因为 Child 和 Parent NodeInjector 是父子关系,子 injector 可以获取到父 injector 的 Provider。

The problem of providers in Projection Content

被 transclude 的 Child 可以 inject 到 Parent 的 providers 未必是一件好事。

试想想,如果你是 Parent 组件,你的 providers 原定是要提供给你的后裔组件的,但是现在外面 transclude 来了一个你完全不可控的 content,

而你的 providers 有可能会渗透进去 content 被里面的组件 inject。这种缺乏隔离性,对富有 Shadow DOM 概念的 Projection Content 来说视乎不太好。

于是,Angular 增设了一个 viewProviders 的属性。

viewProviders 的特色就是它不会渗透到 projection content 里。

Child 组件无法 inject 到 Parent 组件 viewProviders 提供的 Provider。

viewProviders 源码逛一逛

只是蜻蜓点水逛一下就好,本系列不是以分析源码为目标的。

在 inject 查找过程中有一个 lookupTokenUsingNodeInjector 函数。这时已经找到 NodeInjector 了,下一步是找到 Provider 对应的 NodeInjectorFactory,调用 factory 就可以获得 value 了。

searchTokensOnInjector 函数

locateDirectiveOrProvider 函数

这个函数的作用是找出 NodeInjectorFactory 的位置。它在找的过程中会避开 viewProviders(具体它是通过什么手法避开,我就懒得研究了,反正关键就是在这里避开的)。

所以,虽然 Service1 和 Service2 都被记入到了 Parent NodeInjector 资料里,但是 Child 却只能找到 Service1,因为在找的过程 viewProviders 被忽略了。

viewProviders Q & A

  1. viewProviders 只适用于 Projection Content 场景吗?

    是的,viewProviders 是专门给 Projection Content 场景而已的,假如组件没有任何 projection content,那你完全不需要思考任何关于 viewProviders 的事情,用 providers 就可以了。

  2. 后裔组件(不是 projection content)可以 inject 到 viewProviders?

    可以,唯独只有 projection content 才无法 inject 到 viewProviders。

    下面这个是后裔组件,Child 可以 inject 到 Parent 的 viewProviders。

    下面这个是 projection content,Child 无法 inject 到 Parent 的 viewProviders。

host options

viewProviders 是一种限制 provide 范围的配置,组件提供 providers 给后裔组件,但不提供给 projection content 里的组件。

host 则是一种限制 inject 范围的配置。

我们知道 injector 有原型链概念,子 injector 找不到,它会去父 injector 找,一直找到最高层的 injector,无论是 R3Injector 还是 NodeInjector 都有这个概念。

R3Injector 文章,我们学过 2 种限制 inject 范围的配置,它们是 self 和 skipSelf options。这两种配置也适用于 NodeInjector 哦。(这里我就不给例子了)

除了 self 和 skipSelf,NodeInjector 还有一个独有的 host 配置,它也是用来限制 inject 范围的。

调用 inject 函数时传入参数 host: true 就可以了。

那 host 的范围是从哪里到哪里呢?我们看一下源码 lookupTokenUsingNodeInjector 函数

这个 hostElementNode: TNode 就是所谓的 host。

还记得什么是 TNode 吗?

在 bootstrapApplication 过程中,Root TView > Root LView > App TNode > App TView > App LView

每一个组件都有 TNode,先有组件 TNode 才有组件 TView 和 TLView。

组件 TNode、TView、LView 是一套的。

TNode 被存放在 parent TView 中。Root TView.data[25] === App TNode。

同时,组件 LView 本身也保存了对其 TNode 的指针 App LView[5] === App TNode。

上面代码中

let hostTElementNode: TNode|null =
    flags & InjectFlags.Host ? lView[DECLARATION_COMPONENT_VIEW][T_HOST] : null;

组件的 TViewType 是 Component,不是 Embedded,所以组件的 lView[15] === lVIew 也就是指向回自己。

Parent Host = Parent LView 的 parent LView[15][5]

Parent LView 的 parent LView 是 App LView

App LView[15] 是 App LView(因为指向自己)

App LView[5] 是 App TNode

所以 Parent Host = App TNode

再一个

Child Host = Parent LView 的 parent LView[15][5]

Child LView 的 parent LView 也是 App LView。(再次提醒,只要是 whatever.component.html 里的所有组件,它们的 parent LView 都是 Whatever LView)

所以 Child Host 也是等于 App TNode。

结论,组件 host = 组件 parent TNode。需要特别留意的是 projection content 里的组件 parent 指的是谁。

host 限制的范围是只能 inject 这个 host TNode 的 LView 内的 NodeInjector。

Child 组件的 host 是 App TNode,App TNode 的 LView 是 App LView。App LView 内有 Parent NodeInjector 和 Child NodeInjector。

所以 Child 组件内使用 inject({ host: true }) 只能查找 Parent NodeInjector 和 Child NodeInjector,再往上比如 App NodeInjector 的 Provider 就找不到了。

 

DOM Tree !== LView Tree !== NodeInjector Tree

DOM Tree 是游览器最终看见的节点树。

LView Tree 是 Angular 内部维护的逻辑视图树

NodeInjector Tree 是 Angular 内部维护的注入器树

这三棵树有非常密切的关系,而且大部分情况下它们的结构是一致的。

这就造成了初学者会误以为它们总是一致的,但其实在一些场景下,这三棵树的结构是不同的。

DOM Tree !== LView Tree

我们来看一个稍微复杂一点的例子

这里的关键是,Grandchild 组件被 transclude 到了 Parent 组件内,然后又被 transclude 到了 Child 组件内。

下面这个是 DOM Tree 的结构

Angular DevTools 看会比较方便

下面这个是 LView Tree 结构

主要的区别是 projection content 内的组件。Grandchild 和 Descendant 组件的 parent LView 都是 App LView。

切记:Angular Change Detection 是依据 LView Tree 工作的,不是依据 DOM Tree 哦。

LView Tree != NodeInjector Tree

下面这个是 NodeInjector Tree

也可以用 Angular DevTools 查看,不过它是横向的。

两个重点:

  1. Grandchild 组件无法 inject 到 Child 组件。

  2. Parent、Grandchild、Descendant NodeInjector 资料都记入在 App LView 里。

切记,Angular DI 是依据 NodeInjector 工作的,不是依据 LView Tree,更不是依据 DOM Tree。

 

当 NodeInjector 遇上指令 (Directives)

上面的例子都是组件,这里我们来看看指令

组件 + 指令

App 组件内有个 Parent 组件,Parent 组件上有一个 Dir1 指令。

Dir1 指令有一个 Provider -- Service1。

问1:Parent 组件可以 inject(Service1) 吗?

问2:Dir1 指令有属于自己的 NodeInjector 吗?

解答

Parent 组件可以 inject(Service1),Dir1 指令没有属于自己的 NodeInjector。

上图是 App LView,可以看到 Parent NodeInjector 里头包含了 Service1、Parent 组件、Dir1 指令的实例。

也就是说 Parent 组件和 Dir1 指令用的是同一个 NodeInjector。

Element + 指令

App 组件内有个 h1 element,h1 element 上有一个 Dir1 指令和一个 Dir2 指令。

Dir1 指令和 Dir2 指令分别 provide 了 Service1 和 Service2。

问1:Dir1 和 Dir2 指令都可以 inject(Service1 和 Service2) 吗?

问2:Dir1 和 Dir2 指令有属于自己的 NodeInjector 吗?

解答

Dir1 和 Dir2 指令都可以 inject 到 Service1 和 Service2。

Dir1 和 Dir2 都没有属于自己的 NodeInjector,它们共享一个 h1 NodeInjector。

下图是 App TView.data,[26] 是 h1 的 TNode,里面表面了其 injectorIndex 是 27。

下图是 App LView

[27] 是 h1 NodeInjector。 里面有 Service1、Services2、Dir1 和 Dir2 指令的实例。 

小总结

从上面 2 个例子,尤其是第二个,我们可以知道 NodeInjector 是基于 TNode,而不是基于组件或者指令。

没有组件也会有 NodeInjector 出现,多个指令并不会导致有多个 NodeInjector 即便它们每一个都设置了 providers。

当组件,指令用着同一个 NodeInjector 时,它们能 inject 到的 providers 就都是一样的(组件可以 inject 到的,指令也一定可以 inject 到)。

NodeInjector の 指令源码逛一逛

稍微逛一下就好。本系列重点不在源码解析。

上图是 App 组件的模板。里面有组件、element 和 指令。

下面这个是 compile 后样子

这里头有几个关系链。

template 是 app.component.html 模板内容被转换成了 JS 的样子。

dependencies 对应 @Component 里的 imports,它记入模板中依赖了哪些组件和指令。

consts 记入模板中组件的指令 selector。 

模板中的组件通过 index 的方式关联上 consts,consts 再从 dependencies 里的指令 @Directive.selector 匹配中指令。

流程大概是这样,细节我们继续看源码。

在走模板代码大流程前,我们需要先回看 bootstrapApplication 过程中的几个小细节。

app.component.js

我们多留意 consts 和 dependencies,它和指令密切关系。

ɵɵdefineComponent 函数的源码在 definition.ts

继续留意 Definition 里的 consts 和 directiveDefs。

bootstrapApplication 过程中有一个函数 createRootComponentView,里头有一个部分是创建 App 组件 TView。

getOrCreateComponentTView 函数的源码在 shared.ts

createTView 函数

 好,回顾完毕。现在专注回 app.component.js 模板代码的部分

ɵɵelement 函数的源码在 element.ts

ɵɵelementStart 函数

elementStartFirstCreatePass 函数 

getOrCreateTNode 函数源码在 shared.ts

回到 elementStartFirstCreatePass 函数 

resolveDirectives 函数源码在 shared.ts

findDirectiveDefMatches 函数

回到 resolveDirectives 函数

 initializeDirectives 函数之前已经分析过了,这里不再复述。但是!里头有一个超级重要的点,一定要 highlight。

注意看,它是 get or create,也就是说,每一个 TNode 只会有一个 NodeInjector。不管 directives 有多少。 

这也就是为什么 Parent 组件和 Dir1 指令用的会是同一个 NodeInjector。

好,源码逛到这边就可以了。

 

@Attribute Decorator

在 Component 组件 の Angular Component vs Custom Elements 我们学习过 @Attribute Decorator。

它可以获取到组件上的 attribute value。它的使用方式是这样

<app-item name="iPhone 14" />
export class ItemComponent {
  constructor(
    // 1. 在 constructor 使用 @Attribute decorator 获取 name attribute
    @Attribute('name') name: string,
  ) {
    console.log(name); // 'iPhone 14'
  }
}

表面上看它和 Dependency Injection 不一定有关系,毕竟它是 @Attribute 不是 @Inject Decorator。

一直到 Angular v17.3.0 发布了 inject HostAttributeToken

竟然用的是 inject 函数。

@Attribute Decorator 源码逛一逛

run compilation

yarn run ngc -p tsconfig.json

Item 组件 Definition

@Attribute decorator 变成了 ɵɵinjectAttribute 函数,虽然开头都是 inject,但这和我们熟悉的 ɵɵdirectiveInject 函数是不同的。

ɵɵinjectAttribute 函数源码在 di_attr.ts

参数是 TNode 和 Attribute Name

injectAttributeImpl 的源码在 di.ts

Item 组件的 TNode.attrs 长这样

 

EnvironmentInjector

我们知道 Angular 有很多种 Injector 

NullInjector 是独立类型

ChainedInjector 是独立类型 (它是下面两大派的分水岭)

ChainedInjector 以下 (App, Child1, Child2) 都是 NodeInjector 

ChainedInjector 以上 (App Standalone, Root, Platform) 都是 R3Injector

R3Injector 继承自 EnvironmentInjector

EnvironmentInjector 是一个抽象类

所以,我们也可以抽象的说 ChainedInjector 以上 (App Standalone, Root, Platform) 都算是 EnvironmentInjector。

类型上是这样分类,但在日常使用上,当我们说 EnviromentInjector 时,通常指的是 ChainedInjector 以上第一个 EnvironmentInjector。

所以它可能是 App Standalone Injector 或者 Root Injector。(注:App Standalone Injector 不一定会有嘛,只有当 App 是 Standalone 组件同时有 import NgModule 才会有)

而 App Standalone Injector 和 Root Injector 区别不大,所以我们也可以认为 EnviromentInjector == Root Injector。

inject EnviromentInjector (Root Injector) in 组件

假如我们在 App 组件 inject Injector

@Component({
  selector: 'app-root',
  standalone: true,
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [CommonModule],
})
export class AppComponent {
  constructor() {
    const nodeInjector = inject(Injector);
    console.log('This is NodeInjector', nodeInjector);
  }
}

我们会得到一个 NodeInjector

那如果我们想要跳过 NodeInjector 直接拿上层的 Root Injector 可以吗?(注:在做 Dynamic Component 时,通常会需要直接拿 Root Injector,这个以后会教)

可以,只要 inject EnviromentInjector

export class AppComponent {
  constructor() {
    const appStandaloneInjector = inject(EnvironmentInjector);
    console.log('This is App Standalone Injector', appStandaloneInjector);
  }
}

效果

拿到了 App Standalone Injector,它的 parent 是 Root Injector。

假如 App 没有 import CommonModule,那拿到的会是 Root Injector,因为 App Standalone Injector 没有了。

效果

inject EnviromentInjector 的查找过程

每当创建 R3Injector 时,如果有声明 scopes 是 'environment'

R3Injector 内部会自动添加 EnvironmentInjector 匹配自身到 records。

所以,当我们在 App inject EnviromentInjector,它会往上查找,直到第一个有 scopes 'environment' 的 R3Injector,也就是 App Standalone Injector。

查找路线是这样

其它方式 inject EnvironmentInjector (Root Injector)

除了 inject EnvironmentInjector 还可以这样

const appStandAloneInjector = this.injector.get(NgModuleRef).injector;

上面我们提过:如果组件是 Standalone 那么 NgModuleRef.injector 会被偷龙转风从 Root Injector 变成 Standalone Injector。

所以上面通过 inject NgModuleRef.injector 拿到的是 App Standalone Injector。

还有一招是

const rootInjector = this.injector.get(ApplicationRef).injector;

通过 inject ApplicationRef.injector 可以拿到 Root Injector。

注意:它拿到的不是 App Standalone Injector,而是 Root Injector 哦,不管有没有 App Standalone Injector,这里拿到的都只会是 Root Injector。

虽然有很多种方式可以拿到 Root Injector,不过 Best Practice 是用第一种方式 -- inject(EnvironmentInjector)。

 

EnvironmentProviders 和 makeEnvironmentProviders

仔细对比 ApplicationConfig.providers 

和 @Component.providers 

它俩的类型有一点微微的不同。

两个都是 Array,Array 里面都可以放 Provider 类型,但是...ApplicationConfig.providers 的 Array 还可以放 EnviromentProviders 类型。

EnviromentProviders 类型只是一个带有 ɵbrand 属性的对象....一头雾水🤔

Root Provider Best Practice

我们来看一个例子,体会它的用意。

假设我们有一个 Root Level 的 TestService

interface TestServiceConfig {
  autoMapping: boolean;
}

const TEST_SERVICE_CONFIG_TOKEN = new InjectionToken<TestServiceConfig>('TestServiceConfig');

@Injectable({ providedIn: 'root' })
export class TestService {
  constructor() {
    console.log('config', inject(TEST_SERVICE_CONFIG_TOKEN));
  }
}

我们可以 providedIn : 'root', 但是这个 TestService 依赖一个 TestServiceConfig,这个 TestServiceConfig 是依据不同项目需求而定的。

所以...要使用 TestService,我们就需要记得在 ApplicationConfig.providers provide 这个 TestServiceConfig。

这不好记嘛...而且有一种一半一半的感觉...😕

解决方式很简单 -- 统一 provide

export function provideTestService(config: TestServiceConfig): Provider[] {
  return [TestService, { provide: TEST_SERVICE_CONFIG_TOKEN, useValue: config }];
}

我们做一个函数,把 TestService 和 TestServiceConfig 一起 provide 出去,并且在调用函数时需要填入 TestServiceConfig。

这两种方式对使用者来说是有很大区别的。

第一种方式 providedIn: 'root',使用者潜意识会认为,我可以直接 inject,因为 providedIn: 'root' 了嘛,但结果是他其实还需要 provide TestServiceConfig。

第二种方式统一 provide,使用者潜意识会认为,我要 inject TestService 就需要先通过 provideTestService 函数 provide,因为它没有 providedIn: 'root'。

provideTestService 函数的风水问题

export const appConfig: ApplicationConfig = {
  providers: [provideTestService({ autoMapping: true })],
};

对类型敏感的人可能会感觉有点奇怪。

provideTestService 函数返回的是 Array<Provider>,而 ApplicationConfig.providers 接受的类型是 Array<Provider>

那不是应该要 spread operator 一下吗?

providers: [...provideTestService({ autoMapping: true })],

之所以没有类型错误,是因为 Provider 类型的结尾有一个 Array<any> 类型

我们知道 any 不顺风水,所以这里记入隐患 +1。

TestService 的初衷是 provide to Root Injector,但被封装进 provideTestService 函数以后...

使用者也可以 provide to 组件,这违背了初衷啊。隐患再 +1。

用 makeEnvironmentProviders 来解决问题

makeEnvironmentProviders 是一个函数,它是 Angular 专门设计来解决上述 2 个隐患的。

你可以把它理解为一种黑魔法,我们不需要深入理解它具体干了什么。

只要用它来 wrap provideTestService 返回的 Array<Provider> 就可以了 

export function provideTestService(config: TestServiceConfig): EnvironmentProviders {
  return makeEnvironmentProviders([TestService, { provide: TEST_SERVICE_CONFIG_TOKEN, useValue: config }]);
}

返回的类型变成了 EnvironmentProviders

ApplicationConfig.providers 接受的类型是 Array<Provider | EnvironmentProviders>

所以我们不在是匹配 any[] 了,隐患 1 解除。

另外

@Component.providers 接受的类型是 Array<Provider>,而 provideTestService 返回的是 EnvironmentProviders,类型不匹配报错。

这就阻止了 TestService 被不小心 provide 到组件 Level,隐患 2 解除。

 

总结

经过 Dependency Injection 依赖注入 和本篇,我相信你已经对 Angular Dependency Injection 有了深入的了解。

DI 不是什么新奇技术,也没有大家讲的那么神奇。很多时候静下心里,逛一逛源码 (有人带最好),那些云里雾里的东西就会慢慢清楚了。 

往后的教程依然会有 "逛源码" 桥段,希望大家会喜欢🤪。

 

目录

上一篇 Angular 17+ 高级教程 – Change Detection & Ivy rendering engine

下一篇 Angular 17+ 高级教程 – Component 组件 の 生命周期钩子 (Lifecycle Hooks)

想查看目录,请移步 Angular 17+ 高级教程 – 目录

 

 

posted @ 2023-04-13 17:08  兴杰  阅读(245)  评论(0编辑  收藏  举报