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

这篇具有很好参考价值的文章主要介绍了Angular 17+ 高级教程 – Component 组件 の Dependency Injection & NodeInjector。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

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

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

 文章来源地址https://www.toymoban.com/news/detail-837830.html

主要参考

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

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

 

R3Injector, NullInjector, NodeInjector

这世界上不只有 R3Injector。

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

NullInjector

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

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

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

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

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

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

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

在 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) 概念。

我们来看一张图

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

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

Null Injector

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

Platform Injector

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

什么叫跨 Application? 

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

比如

index.html

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

main.ts

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

app.component.ts 和 app2.component.ts

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

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

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

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。

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

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

Platform Injector 是一种 R3Injector

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

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

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'。

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

App Standalone Injector

当 App 是 Standalone 组件时,就会有多一个 App Standalone Injector,如果用 NgModule 可能就不会有了 (我也不确定),NgModule 以后才会教。

关于这个 App Standalone Injector 具体有什么特别之处,我也不晓得,我们先忽视它吧。只要知道它是一个 R3Injector,它的 parent 是 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。

Angular 17+ 高级教程 – Component 组件 の Dependency Injection & 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。

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

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

我们在 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 是一样的
})
export class AComponent {}

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

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

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

 

深入理解 NodeInjector

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

怎么说呢?看例子

Angular 17+ 高级教程 – Component 组件 の Dependency Injection & 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

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

main.js 长这样

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

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

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

createProvidersConfig 函数

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

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

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

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

internalCreateApplication 函数

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

createOrReusePlatformInjector 函数

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

createPlatformInjector 函数

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

回到 internalCreateApplication 函数

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

EnvironmentNgModuleRefAdapter 类

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

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

回到 internalCreateApplication 函数

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

ApplicationRef 类

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

回到 internalCreateApplication 函数

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

bootstrap 函数

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

create 函数的源码在 component_ref.ts

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

ChainedInjector 类

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

回到 create 函数

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

继续

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

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。

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

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

一个 h1,一个 text

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

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

上图就是 App TVIew.data。

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

这个 TNode.type 是这样看的

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

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

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

下图是 App LView

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

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

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

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

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

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

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

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

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

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 来实现的。

它类似于下图

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

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

好,到这里我们已经知道了组件的 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

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

回到 create 函数

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

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

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

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

回到 create 函数

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

createRootComponentView 函数

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

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

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

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

回到 create 函数

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

createRootComponent 函数

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

initializeDirectives 函数的源码在 shared.ts

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

getOrCreateNodeInjectorForNode 函数的源码在 di.ts

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

insertBloom 函数

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

回到 getOrCreateNodeInjectorForNode 函数

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

回到 initializeDirectives 函数

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

diPublicInInjector 函数的源码在 di.ts

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

bloomAdd 函数

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

回到 initializeDirectives 函数

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

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

app.component.js

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

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

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

回到 app.component.js

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

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

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

initFeatures 函数

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

回到 initializeDirectives 函数

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

providersResolver 函数的源码在 di_setup.ts

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

resolveProvider 函数

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

继续

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

继续

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

回到 initializeDirectives 函数

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

继续

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

configureViewWithDirective 函数

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

getFactoryDef 函数的源码在 definition_factory.ts

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

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

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

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

compileComponent 函数

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

addDirectiveFactoryDef 函数

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

app.component.js

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

回到 configureViewWithDirective 函数

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

继续

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

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

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

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

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

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

回到 createRootComponent 函数

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

getNodeInjectable 函数源码在 di.ts

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

isFactory 函数源码在 injector.ts

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

回到 getNodeInjectable 函数

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

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

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

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

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

class NodeInjectorFactory 源码在 injector.ts

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

回到 getNodeInjectable 函数

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

继续

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

factory.factory 长啥样?

回顾 class NodeInjectorFactory 

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

回顾 configureViewWithDirective 函数

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

小总结一下,

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

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

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

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

回到 createRootComponent 函数

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

回到 ComponentFactory.create 函数

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

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

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

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

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 文章中 有提到过。

我们继续看完它。

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

回到 ApplicationRef.bootstrap 函数

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

_loadComponent 方法

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

上面我们有提到,到目前为止,我们只完成了 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 做准备。

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

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

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

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。

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

如果不是 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。

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

包括 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 编号了。

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

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

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

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

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

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

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

Provider 的 ids

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

它们各个的 NodeInjectorBloomCode

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

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

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

Parent 的 15 来自 App TView.data 的 15

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

 

inject 函数查找过程

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

有 4 个不同的例子

例子一:Child 组件注入 Child Provider

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

第 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)。

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

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

第 4 步

有了 injector index 就去拿 injector 编号。

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

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。

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

从 36 开始一个一个找。

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

第 7 步

到 Parent LView[37] 获取 NodeInjectorFactory

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

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

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

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

第 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 资料

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

第 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 位置

Angular 17+ 高级教程 – Component 组件 の Dependency Injection & 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 编号

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

第 9 步

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

拿 Parent NodeInjector 编号和 Provider 编号比对

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

一样是 unmatched。继续往上找

这个是 App LView 

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

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] 

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

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。

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

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

第 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

Angular 17+ 高级教程 – Component 组件 の Dependency Injection & 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 函数源码逛一逛

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

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

组件内会调用 inject 函数

inject 函数源码在 injector_compatibility.ts

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

ɵɵinject 函数

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

getInjectImplementation 函数的源码在 inject_switch.ts

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

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

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

回到 ɵɵinject 函数

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

我们之前就讲过很多次了,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 函数

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

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

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

ɵɵdirectiveInject 函数源码在 di.ts

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

getOrCreateInjectable 函数源码在 di.ts

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

lookupTokenUsingNodeInjector 函数

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

bloomHashBitOrFactory 函数

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

回到 lookupTokenUsingNodeInjector 函数

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

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,而是函数。

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

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

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

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

 

当 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

例子

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

有 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 哦。

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

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

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

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

  4. Child NodeInjector 的 parent Injector 是谁?

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

    下图是 App LView

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

viewProviders

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

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

问: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 的属性。

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

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

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

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

viewProviders 源码逛一逛

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

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

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

searchTokensOnInjector 函数

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

locateDirectiveOrProvider 函数

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

这个函数的作用是找出 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。

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

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

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

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

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 范围的。

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

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

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

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

这个 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;

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

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

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

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

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 就找不到了。

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

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

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

 

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

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

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

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

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

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

DOM Tree !== LView Tree

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

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

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

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

下面这个是 DOM Tree 的结构

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

用 Angular DevTools 看会比较方便

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

下面这个是 LView Tree 结构

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

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

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

LView Tree != NodeInjector Tree

下面这个是 NodeInjector Tree

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

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

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

两个重点:

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

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

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

 

当 NodeInjector 遇上指令 (Directives)

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

组件 + 指令

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

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

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

Dir1 指令有一个 Provider -- Service1。

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

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

解答

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

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

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

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

Element + 指令

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

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

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

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。

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

下图是 App LView

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

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

小总结

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

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

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

NodeInjector の 指令源码逛一逛

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

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

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

下面这个是 compile 后样子

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

这里头有几个关系链。

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

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

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

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

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

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

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

app.component.js

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

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

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

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

继续留意 Definition 里的 consts 和 directiveDefs。

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

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

getOrCreateComponentTView 函数的源码在 shared.ts

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

createTView 函数

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

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

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

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

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

ɵɵelementStart 函数

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

elementStartFirstCreatePass 函数 

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

getOrCreateTNode 函数源码在 shared.ts

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

回到 elementStartFirstCreatePass 函数 

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

resolveDirectives 函数源码在 shared.ts

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

findDirectiveDefMatches 函数

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

回到 resolveDirectives 函数

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

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

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

注意看,它是 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

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

竟然用的是 inject 函数。

@Attribute Decorator 源码逛一逛

run compilation

yarn run ngc -p tsconfig.json

Item 组件 Definition

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

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

ɵɵinjectAttribute 函数源码在 di_attr.ts

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

参数是 TNode 和 Attribute Name

injectAttributeImpl 的源码在 di.ts

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

Item 组件的 TNode.attrs 长这样

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

 

 

目录

上一篇 Angular 17+ 高级教程 – Signals

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

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

 

 

到了这里,关于Angular 17+ 高级教程 – Component 组件 の Dependency Injection & NodeInjector的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

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

    之前在 Component 组件 の Angular Component vs Custom Elements 文章中,我们有学习过几个基础的 Lifecycle Hooks。 比如 OnChanges、OnInit、AfterViewInit、OnDestroy,但那篇只是微微带过而已。 这篇让我们来深入理解 Angular 的 Lifecycle Hooks。   在 Component 组件 の Dependency Injection NodeInjector 文章中

    2024年03月09日
    浏览(51)
  • Angular 17+ 高级教程 – Component 组件 の Structural Directive (结构型指令) & Syntax Reference (微语法)

    在 Attribute Directives 属性型指令 文章中,我们学习过了指令。指令是没有 HTML 和 CSS 的组件,它单纯用于封装 JS 的部分。 这一篇我们将继续学习另一种指令 -- Structural Directive 结构型指令。 就代码而言,Structural Directive 和 Attribute Directives 是完全一样的,只是用途不同,因此

    2024年03月10日
    浏览(49)
  • Angular(二) Understanding Dependency Injection for angular

    Angular https://angular.io/guide/dependency-injection Denpendency Injection,  or DI, is one of fundamental concepts for angular, DI is writed by angular framework and allows classes with  Angular decorators,  such as Components, directives, Piples and Injectables , to configure dependencies that they need.  Two main roles exists in DI system: dependency

    2024年02月09日
    浏览(39)
  • Understanding Dependency Injection for angular

    Angular https://angular.io/guide/dependency-injection Denpendency Injection,  or DI, is one of fundamental concepts for angular, DI is writed by angular framework and allows classes with  Angular decorators,  such as Components, directives, Piples and Injectables , to configure dependencies that they need.  Two main roles exists in DI system: dependency

    2024年02月10日
    浏览(34)
  • Angular 17+ 高级教程 – HttpClient

    HttpClient 是 Angular 对 XMLHttpRequest 和 Fetch 的封装。 HttpClient 的 DX (Developer Experience) 比 XMLHttpRequest 和 Fetch 都好,只是学习成本比较高,因为它融入了 RxJS 概念。 要深入理解 HttpClient 最好先掌握 3 个基础技能: XMLHttpRequest -- 看这篇 Fetch -- 看这篇 RxJS -- 看这系列 (如果只是为了

    2024年03月16日
    浏览(53)
  • Angular 17+ 高级教程 – NgModule

    NgModule 在 Angular v14 以前是一门必修课。然而,自 Angular v14 推出 Standalone Component 以后,它的地位变得越来越边缘化了。 本教程从开篇到本篇,所有例子使用的都是 Standalone Component,一点 NgModule 的影子也没有😔。 但是!NgModule 还是有价值的,而且在越复杂的项目中你越可以

    2024年03月12日
    浏览(62)
  • Angular 17+ 高级教程 – Library

    当你需要管理超过一个项目时,你就需要知道怎么使用 Angular Library。 你可以把多个项目共享的组件放到这个 Library 了,就像 Angular Material 那样。   Sandro Roth – Building an Angular Library with multiple entry points (主要参考) Docs – Creating libraries Docs – Copy assets Docs – Embed assets in C

    2024年04月17日
    浏览(39)
  • Angular 17+ 高级教程 – Signals

    在上一篇 Change Detection 中, 我们有提到 MVVM 监听 ViewModel 变化的难题. 当年 AngularJS 和 Knockout.js (下面简称 KO) 各自选了不同的道路. 但如今, 事过境迁, Angular 最终也走向了 KO 的道路. 这就是这篇的主角 Signal。   在 JavaScript, 值类型 variable 无法被监听, Signal 的做法是把它们都变

    2024年03月09日
    浏览(67)
  • Angular 17+ 高级教程 – 盘点 Angular v14 到 v17 的重大改变

    我在 初识 Angular 文章里有提到 Angular 目前的断层问题。 大部分的 Angular 用户都停留在 v9.0 版本。 v9.0 是一个里程碑版本,Angular 从 v4.0 稳定版推出后,好几年都没有什么动静,直到 v9.0 推出了 Ivy rendering engine。 本以为 v9.0 以后 Angular 会大爆发,结果迎来的是 Angular 团队搞内

    2024年04月22日
    浏览(36)
  • Angular 17+ 高级教程 – Change Detection

    虽然 Angular 正在把大部分 Change Detection 概念换成 Signal,但是最快也要 1 年后,所以还是有必要认真学习一下的。   MVVM 框架的开发方式是这样的: 写 HTML 写 ViewModel 在 HTML 里加入 binding syntax。 在 HTML 里加入 listening syntax,在事件发生时修改 ViewModel。 MVVM 的宗旨是 \\\"不要直接

    2024年03月09日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包