摘要:裝飾器我們為啥要討論元素注入器而不是裝飾器這是因為會把元素注入器依賴解析過程限制在當前組件視圖內。但是一旦使用了裝飾器,整個依賴解析過程就會在第一階段完成后停止解析,也就是說,元素注入器只在組件視圖內解析依賴,然后就停止解析工作。
原文鏈接: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: ``, 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 兩者之間的關系:
每一個視圖節點都是由節點定義對象實例化后創建的,節點定義對象包含描述節點的元數據。比如,像 element 類型的節點通常用來表示 DOM 元素,而這些元數據是由 @angular/compiler 的編譯器編譯組件模板和附著在模板上的指令生成的。下圖表示視圖節點定義與其對象之間的關系:
元素節點定義描述了一個有趣的功能:
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: `` }) 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 屬性,包含兩個服務提供者 ADirective 和 BDirective,該屬性作用類似于一個注入器,而 referenceToADirectiveProviderDefinition 和 referenceToBDirectiveProviderDefinition 就是附著在 div 元素上兩個指令的實例。由于它們是由同一個元素注入器解析,所以 你可以把其中一個指令注入到另一個指令中。當然,你不能在兩個指令中相互注入依賴,因為這會導致依賴死鎖。
所以,下圖說明了我們現在擁有的東西:
注意宿主元素 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 } } }
再一次放圖,下圖是現在的結果:
多級元素注入器上文我們只有一個 HTML 元素,嵌套 HTML 元素組成了 DOM 元素層級,組件視圖內的這些 DOM 元素組成了 Angular 依賴注入系統內多級元素注入器。
讓我們看示例。
假設組件模板中有父子元素 div,同時,還有兩個指令 A 和 B。A 指令附著在父元素 div 上并提供 ADirService 服務,B 指令附著在子元素 div 上但不提供任何服務。
下面代碼展示了具體內容:
@Component({ selector: "my-app", template: `` }) 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 結構創建了多級元素注入器
有趣的是,子組件也創建了一個元素注入器,成了多級元素注入器的一部分,例如,如下代碼:
adir 指令提供一個服務,創建了兩個層級元素注入器——上層級父注入器創建在 div 元素上,下層級子注入器創建在 a-comp 元素上,這并不奇怪,因為 組件也僅僅是帶有指令的 HTML 元素。
創建元素注入器當 Angular 為嵌套 HTML 元素創建注入器時,該注入器要么會繼承父注入器,要么直接把父注入器賦值給子注入器。如果子元素上掛載指令且該指令提供依賴服務,則子注入器會繼承父注入器,也就是說,由掛載指令并提供服務的子元素創建的元素注入器,是會繼承父注入器的。另外,沒有必要為子組件多帶帶創建一個注入器,如有必要,可以直接使用父注入器來解析依賴。
下圖說明了這個過程:
依賴解析過程安裝組件視圖內的多級元素注入器,會大大簡化依賴解析過程。Angular 使用 JavaScript 的原型鏈的屬性查詢機制來解析依賴,而不是一層層去查找父注入器去解析依賴:
elDef.element.publicProviders[tokenKey]
由于 JavaScript 的工作方式,訪問 publicProviders 對象 key 對應的值,會直接從父元素注入器或原型鏈中解析出來。
@Host 裝飾器我們為啥要討論元素注入器而不是 @Host 裝飾器?這是因為 @Host 會把元素注入器依賴解析過程限制在當前組件視圖內。在一般依賴解析過程中,如果組件視圖內的元素注入器不能解析一個令牌,Angular 依賴解析系統會遍歷父視圖,去使用組件/視圖注入器來解析令牌,如果還沒找到依賴,則遍歷模塊注入器去查找依賴。但是一旦使用了 @Host 裝飾器,整個依賴解析過程就會在第一階段完成后停止解析,也就是說,元素注入器只在組件視圖內解析依賴,然后就停止解析工作。
示例@Host 在表單指令里被大量使用,比如,往 ngModel 指令里注入一個表單容器,并把由該指令實例化的表單控件對象(FormControl)注入到表單對象內,典型的模板驅動表單代碼如下:
實際上, 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: ``, 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,可以照著本文敘述玩一玩。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/96538.html
摘要:注入器樹大多數開發者知道,會創建根注入器,根注入器內的服務都是單例的。也會被添加到這個根注入器對象內,它主要用來創建動態組件,因為它存儲了屬性指向的組件數組。為了理解依賴解析算法,我們首先需要知道視圖和父視圖元素概念。 What you always wanted to know about Angular Dependency Injection tree showImg(https...
摘要:三的洋蔥模型這里簡單講講在中是如何分層的,也就是說請求到達服務端后如何層層處理,直到響應請求并將結果返回客戶端。從而使得端支持跨域等。 ??最近已經使用過一段時間的nestjs,讓人寫著有一種java spring的感覺,nestjs可以使用express的所有中間件,此外完美的支持typescript,與數據庫關系映射typeorm配合使用可以快速的編寫一個接口網關。本文會介紹一下作...
摘要:三的洋蔥模型這里簡單講講在中是如何分層的,也就是說請求到達服務端后如何層層處理,直到響應請求并將結果返回客戶端。從而使得端支持跨域等。 ??最近已經使用過一段時間的nestjs,讓人寫著有一種java spring的感覺,nestjs可以使用express的所有中間件,此外完美的支持typescript,與數據庫關系映射typeorm配合使用可以快速的編寫一個接口網關。本文會介紹一下作...
摘要:在探索抽象類前,先了解下如何在組件指令中獲取這些抽象類。下面示例描述在組建模板中如何創建如同其他抽象類一樣,通過屬性綁定元素,比如上例中,綁定的是會被渲染為注釋的元素,所以輸出也將是。你可以使用查詢模板引用變量來獲得抽象類。 原文鏈接:Exploring Angular DOM manipulation techniques using ViewContainerRef如果想深入學習 ...
閱讀 2977·2021-11-23 09:51
閱讀 3609·2021-10-13 09:39
閱讀 2491·2021-09-22 15:06
閱讀 881·2019-08-30 15:55
閱讀 3147·2019-08-30 15:44
閱讀 1778·2019-08-30 14:05
閱讀 3434·2019-08-29 15:24
閱讀 2362·2019-08-29 12:44