[译] Angular 的 @Host 装潢器和元素注入器

原文链接:
A curious case of the @Host decorator and Element Injectors in Angular

我们晓得,Angular 依靠注入机制包含 @Optional@Self影响依靠剖析历程的装潢器,只管它们字面意义就直接诠释了其作用,然则 @Host 却搅扰了我良久。我在其源码解释中看到该装潢器的 形貌

Specifies that an injector should retrieve a dependency from any injector until reaching the host element of the current component.

由于网上大多数教程都提到 Angular 的模块注入器和组件注入器,所以我以为 @Host 应当和多级组件注入器相干。我猜测 @Host 装潢器能够用在子组件内,来限定只能在它本身和其父组件注入器内剖析依靠,所以我做了个 小示例 来考证这个假定:

@Component({
    selector: 'my-app',
    template: `<a-comp></a-comp>`,
    providers: [MyAppService]
})
export class AppComponent {}

@Component({selector: 'a-comp', ...})
export class AComponent {
    constructor(@Host() s: MyAppService) {}
}

然则竟然报错 No provider for MyAppServic,有意义的是,假如我 移除 @Host 装潢器,MyAppService 就能够顺遂从父组件注入器内剖析出来。发作了什么?为了弄清楚,我撸起袖子最先观察。让我与你分享我终究发明了什么。

我如今就通知你症结点就在上文 @Host 装潢器形貌中的 ‘until’ 一词上:

…retrieve a dependency from any injector
until reaching the host element

它意义是 @Host 装潢器会让依靠剖析历程限定在当前组件模板,甚至都不包含其宿主元素(注:在宿主元素 a-comp 上绑定含有 MyAppService 效劳的指令 ADirective,是能够在 AComponent 的组织函数中剖析出被 @Host 装潢的 MyAppService 效劳)。这就是我的示例中毛病缘由——Angular 不会从其宿主父组件注入器中剖析依靠。

所以如今我们晓得 @Host 装潢器不能够用来在子组件中剖析来自父组件的依靠供应者,意味着该装潢器的依靠剖析机制不能够用于多级组件注入器。

所以应当运用什么样的多级注入器?

实际上,除了模块注入器和组件注入器,Angular 还供应了第三种注入器,即多级元素注入器,它是由 HTML 元素和指令配合建立的

元素注入器

Angular 会根据三个阶段来剖析依靠,肇端阶段就是运用多级元素注入器,然后是多级组件注入器,末了是多级模块注入器。假如你对全部剖析历程感兴趣,我强烈建议你浏览 Alexey Zuev 写的这篇 深度好文

对组件和模块注入器剖析依靠的后两个阶段,我们应当很熟悉了。当你耽误加载模块时,Angular 会建立多级模块注入器,细致历程我已在 my talk at NgConf 做了演讲,并 写了篇文章多级组件注入器是由模板中嵌套组件建立的,就内部完成而言,组件注入器也可称为视图注入器,稍后将会诠释缘由。

别的,多级元素注入器是 Angular 依靠注入体系内很少晓得的功用,由于文档里没写,然则这类注入器在依靠注入体系的肇端阶段就运用了。这些多级元素注入器被用来剖析由 @Host 装潢的依靠,所以让我们细致研讨下这类注入器。

一个元素注入器

你可能从我 之前一篇文章 中晓得,Angular 内部运用一种叫视图组件视图的数据构造来示意组件(注:可检察源码中 ViewDefinition 接口)。实际上,这就是把组件注入器称为视图注入器的由来,视图对象重要用来示意组件模板中由 HTML 元素建立的 DOM 节点的鸠合(注:@angular/compiler 会编译你写的组件,天生的效果由 ViewDefinition 接口来示意,不需要晓得其编译历程,只需晓得你写的带有 @Component 装潢的类不编译为新的类是没法直接运转的,且 HTML 模板就存在于新类的属性里,即 Compile HTML+Class into New Class。正由于 @angular/compiler 编译功用强大,所以能够在 HTML 模板里写许多不符合 HTML 语法的 HTML 代码,比方绑定指令等等)。所以,每个视图对象内部是由差别品种视图节点构成的(注:ViewDefinition 示意编译后的视图对象,视图又是由节点构成的,@angular/core 运用 NodeDef 接口来示意一切节点,而节点又分许多种,运用 NodeFlags 来示意,个中最常见的 TypeElement 范例就是来标识 HTML 元素,运用 ElementDef 接口来示意),最经常使用的视图节点范例是元素节点,用来示意对应的 DOM 元素,下图示意视图和 DOM 两者之间的关联:

《[译] Angular 的 @Host 装潢器和元素注入器》

每个视图节点都是由节点定义对象实例化后建立的,节点定义对象包含形貌节点的元数据。比方,像 element 范例的节点通经常使用来示意 DOM 元素,而这些元数据是由 @angular/compiler 的编译器编译组件模板和附着在模板上的指令天生的。下图示意视图节点定义与其对象之间的关联:

《[译] Angular 的 @Host 装潢器和元素注入器》

元素节点定义形貌了一个风趣的功用:

In Angular, a node definition that describes an HTML element defines its own injector. In other words, an HTML element in a component’s template defines its own element injector. And this injector can populated with providers by applying one or more directives on the corresponding HTML element.

让我们看示例。

假定组件模板中有个 div 元素,并且有两个指令挂载到上面:

@Component({
    selector: 'my-app',
    template: `<div a b></div>`
})
export class AppComponent {}

@Directive({ selector: '[a]' })
export class ADirective {}

@Directive({ selector: '[b]' })
export class BDirective {}

@angular/compiler 的编译器会编译模板天生视图,该视图中 DOM 元素 div 对应的元素节点定义对象包含以下元数据(注:可检察 ElementDef 接口中的 name 和 publicProviders 属性):

const DivElementNodeDefinition = {
    element: {
        name: 'div',
        publicProviders: {
            ADirective: referenceToADirectiveProviderDefinition,
            BDirective: referenceToBDirectiveProviderDefinition
        }
    }
}

正如你所见,节点对象定义了 element.publicProviders 属性,包含两个效劳供应者 ADirectiveBDirective,该属性作用类似于一个注入器,而 referenceToADirectiveProviderDefinitionreferenceToBDirectiveProviderDefinition 就是附着在 div 元素上两个指令的实例。由于它们是由同一个元素注入器剖析,所以 你能够把个中一个指令注入到另一个指令中。固然,你不能在两个指令中互相注入依靠,由于这会致使依靠死锁。

所以,下图说清楚明了我们如今具有的东西:

《[译] Angular 的 @Host 装潢器和元素注入器》

注重宿主元素 app-comp 存在于 AppComponentView 以外,由于它是属于父组件视图内的。

如今假如 ADirective 也包含效劳供应者会发作什么?

@Directive({
    selector: '[a]',
    providers: [ADirService]
})
export class ADirective {}

正如你所料,这个效劳会被包含进由 div 建立的元素注入器里:

const divElementNodeDefinition = {
    element: {
        name: 'div',
        publicProviders: {
            ADirService: referenceToADirServiceProviderDefinition
            ADirective: referenceToADirectiveProviderDefinition,
            ADirective: referenceToADirectiveProviderDefinition
        }
    }
}

再一次放图,下图是如今的效果:

《[译] Angular 的 @Host 装潢器和元素注入器》

多级元素注入器

上文我们只要一个 HTML 元素,嵌套 HTML 元素构成了 DOM 元素层级,组件视图内的这些 DOM 元素构成了 Angular 依靠注入体系内多级元素注入器。

让我们看示例。

假定组件模板中有父子元素 div,同时,另有两个指令 ABA 指令附着在父元素 div 上并供应 ADirService 效劳,B 指令附着在子元素 div 上但不供应任何效劳。

下面代码展现了具体内容:

@Component({
    selector: 'my-app',
    template: `
        <div a>
            <div b></div>
        </div>
    `
})
export class AppComponent {}
@Directive({
    selector: '[a]',
    providers: [ADirService]
})
export class ADirective {}
@Directive({ selector: '[b]' })
export class BDirective {}

假如我们去探讨 @angular/compiler 编译模板建立的元素节点定义对象,会发明存在两个 element 范例节点来形貌 div 元素:

const viewDefinitionNodes = [
    {
        // element definition for the parent div
        element: {
            name: `div`,
            publicProviders: {
                ADirective: referenceToADirectiveProviderDefinition,
                ADirService: referenceToADirServiceProviderDefinition,
            }
        }
    },
    {
        // element definition for the child div
        element: {
            name: `div`,
            publicProviders: {
                BDirective: referenceToBDirectiveProviderDefinition
            }
        }
    }
]

正如上文中发明的,每个 div 元素定义都有个 publicProviders 作为依靠注入容器。由于附着在父 div 元素还供应了 ADirService 效劳,所以该效劳也被加到父元素 div 的元素注入器内。

嵌套 HTML 构造建立了多级元素注入器

风趣的是,子组件也建立了一个元素注入器,成了多级元素注入器的一部分,比方,以下代码:

<div adir>
    <a-comp></a-comp>
</div>

adir 指令供应一个效劳,建立了两个层级元素注入器——上层级父注入器建立在 div 元素上,下层级子注入器建立在 a-comp 元素上,这并不新鲜,由于 组件也仅仅是带有指令的 HTML 元素

建立元素注入器

当 Angular 为嵌套 HTML 元素建立注入器时,该注入器要么会继承父注入器,要么直接把父注入器赋值给子注入器。假如子元素上挂载指令且该指令供应依靠效劳,则子注入器会继承父注入器,也就是说,由挂载指令并供应效劳的子元素建立的元素注入器,是会继承父注入器的。别的,没有必要为子组件零丁建立一个注入器,若有必要,能够直接运用父注入器来剖析依靠。

下图说清楚明了这个历程:

《[译] Angular 的 @Host 装潢器和元素注入器》

依靠剖析历程

装置组件视图内的多级元素注入器,会大大简化依靠剖析历程。Angular 运用 JavaScript 的原型链的属性查询机制来剖析依靠,而不是一层层去查找父注入器去剖析依靠:

elDef.element.publicProviders[tokenKey]

由于 JavaScript 的事变体式格局,接见 publicProviders 对象 key 对应的值,会直接从父元素注入器或原型链中剖析出来。

@Host 装潢器

我们为啥要议论元素注入器而不是 @Host 装潢器?这是由于 @Host 会把元素注入器依靠剖析历程限定在当前组件视图内。在平常依靠剖析历程当中,假如组件视图内的元素注入器不能剖析一个令牌,Angular 依靠剖析体系会遍历父视图,去运用组件/视图注入器来剖析令牌,假如还没找到依靠,则遍历模块注入器去查找依靠。然则一旦运用了 @Host 装潢器,全部依靠剖析历程就会在第一阶段完成后住手剖析,也就是说,元素注入器只在组件视图内剖析依靠,然后就住手剖析事变。

示例

@Host 在表单指令里被大批运用,比方,往 ngModel 指令里注入一个表单容器,并把由该指令实例化的表单控件对象(FormControl)注入到表单对象内,典范的模板驱动表单代码以下:

<form>
    <input ngModel>
</form>

实际上, NgForm 指令的选择器与 form DOM 元素婚配,该指令包含一个效劳供应者,把本身注册为 ControlContainer 令牌指向的效劳:

@Directive({
    selector: 'form',
    providers: [
        {
            provide: ControlContainer,
            useExisting: NgForm
        }
    ]
})
export class NgForm {}

ngModel 指令也是用 ControlContainer 令牌来注入依靠,并将本身注册为表单的一个控件:

@Directive({
    selector: '[ngModel]',
})
export class NgModel {
    constructor(@Optional() @Host() parent: ControlContainer) {}
    private _setUpControl(): void {
        ...
        this.parent.formDirective.addControl(this);
    }
}

正如你所见,@Host 装潢器会把依靠剖析历程限定在当前组件视图内,大多数状况下,这是预期的状况,然则有时候你需要在嵌套表单里注入来自父组件的表单对象。Alexey Zuev 找到相识决方案并 写了篇文章

上文说到的文章还提到了另一个有意义的事变,假如我稍稍修改文章开首说到的谁人示例,把 MyAppService 注册在 viewProviders 而不是 providers 里:

@Component({
    selector: 'my-app',
    template: `<a-comp></a-comp>`,
    viewProviders: [MyAppService]
})
export class AppComponent {}

@Component({selector: 'a-comp', ...})
export class AComponent {
    constructor(@Host() s: MyAppService) {}
}

MyAppService 依靠就能够从父组件中被胜利的剖析出来。

这是由于 Angular 在剖析由 @Host 装潢的依靠时,会针对当前组件(注:原文是父组件,应当是当前组件)的 viewProviders分外的搜检

// check @Host restriction
if (!result) {
    if (!dep.isHost || this.viewContext.component.isHost ||
        this.viewContext.component.type.reference === tokenReference(dep.token !) ||
        // this line
        this.viewContext.viewProviders.get(tokenReference(dep.token !)) != null) { <------
        result = dep;
    } else {
        result = dep.isOptional ? result = {isValue: true, value: null} : null;
    }
}

本文作者叙说体式格局有点乱,轻易致使不相识 Angular 依靠注入(Dependency Injection)的人被搞的一脸懵逼。总之,DI 体系根据运用递次包含三种注入器:元素注入器,组件注入器和模块注入器,而 @Host 装潢器会限定只运用元素注入器来剖析依靠,假如当前组件依靠于被 @Host 润饰的依靠,或模板被绑定了指令且该指令依靠于被 @Host 润饰的依靠,就会报错剖析不了该依靠(由于方才说了,@Host 装潢器会限定只运用元素注入器来剖析依靠,不会继承运用组件注入器从父组件那拿依靠),解决方案是能够在当前组件的 viewProviders 属性中供应这个依靠(由于源码中写了 @Host 润饰的依靠会末了还从 viewProviders 属性中看看有没有这个依靠)。写了个
stackblitz demo,能够照着本文叙说玩一玩。

    原文作者:lx1036
    原文地址: https://segmentfault.com/a/1190000015862547
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞