[譯] 關於 Angular 依賴注入你須要知道的

What you always wanted to know about Angular Dependency Injection treenode

DI Tree

若是你以前沒有深刻了解 Angular 依賴注入系統,那你如今可能認爲 Angular 程序內的根注入器包含全部合併的服務提供商,每個組件都有它本身的注入器,延遲加載模塊有它本身的注入器。git

可是,僅僅知道這些可能還不夠呢?github

不久前有個叫 Tree-Shakeable Tokens feature 被合併到 master 分支,若是你和我同樣充滿好奇心,可能也想知道這個 feature 改變了哪些東西。算法

因此如今去看看,可能有意外收穫嗷。bootstrap

注入器樹(Injector Tree)

大多數開發者知道,Angular 會建立根注入器,根注入器內的服務都是單例的。可是,貌似還有其餘注入器是它的父級。小程序

做爲一名開發者,我想知道 Angular 是怎麼構建注入器樹的,下圖是注入器樹的頂層部分:api

Top of DI Tree

這不是整棵樹,目前尚未任何組件呢,後面會繼續畫樹的剩餘部分。可是如今先看下根注入器 AppModule Injector,由於它是最常使用的。數組

根注入器(Root AppModule Injector)

咱們知道 Angular 程序根注入器 就是上圖的 AppModule Injector,上文說了,這個根注入器包含全部中間模塊的服務提供商,也就是說(注:不翻譯):bash

If we have a module with some providers and import this module directly in AppModule or in any other module, which has already been imported in AppModule, then those providers become application-wide providers.app

根據這個規則,上圖中 EagerModule2MyService2 也會被包含在根注入器 AppModule Injector 中。

ComponentFactoryResolver 也會被 Angular 添加 到這個根注入器對象內,它主要用來建立動態組件,由於它存儲了 entryComponents 屬性指向的組件數組。

值得注意的是,全部服務提供商中有 Module Tokens,它們都是被導入模塊的類名。後面探索到 tree-shakeable tokens 時候,還會回到這個 Module Tokens 話題。

Angular 使用 AppModule 工廠函數來實例化根注入器 AppModule Injector,這個 AppModule 工廠函數就在所謂的 module.ngfactory.js 文件內:

AppModule Factory

咱們能夠看到這個工廠函數返回一個包含全部被合併服務提供商的模塊對象,全部開發者都應當熟悉這個(注:能夠查看 譯 Angular 的 @Host 裝飾器和元素注入器)。

Tip: If you have angular application in dev mode and want to see all providers from root AppModule injector then just open devtools console and write:

ng.probe(getAllAngularRootElements()[0]).injector.view.root.ngModule._providers
複製代碼

Providers of Root Injector

還有不少其餘知識點,我在這裏沒有描述,由於官網上已經談到了:

angular.io/guide/ngmod…

angular.io/guide/hiera…

Platform Injector

實際上,根注入器 AppModule Injector 有個父注入器 NgZoneInjector,而它又是 PlatformInjector 的子注入器。

PlatformInjector 會在 PlatformRef 對象初始化的時候,包含內置的服務提供商,但也能夠額外包含服務提供商:

const platform = platformBrowserDynamic([ { 
  provide: SharedService, 
  deps:[] 
}]);
platform.bootstrapModule(AppModule);
platform.bootstrapModule(AppModule2);
複製代碼

這些額外的服務提供商是由咱們開發者傳入的,且必須是 StaticProviders。若是你不熟悉 StaticProvidersProvider 二者間的區別,能夠查看這個 StackOverflow 的答案

Tip: If you have angular application in dev mode and want to see all providers from Platform injector then just open devtools console and write:

ng.probe(getAllAngularRootElements()[0]).injector.view.root.ngModule._parent.parent._records;

// to see stringified value use
ng.probe(getAllAngularRootElements()[0]).injector.view.root.ngModule._parent.parent.toString();
複製代碼

Providers of Platform Injector

儘管根注入器以及其父注入器解析依賴的過程清晰明瞭,可是組件級別的注入器如何解析依賴卻讓我很困惑,因此,我接着去深刻了解。

EntryComponent and RootData

我在上文聊到 ComponentFactoryResolver 時,就涉及到 entryComponents 入口組件。這些入口組件會在 NgModule 的 bootstrapentryComponents 屬性中聲明,@angular/router 也會用它們動態建立組件。

Angular 會爲全部入口組件建立宿主工廠函數,這些宿主工廠函數就是其餘視圖的根視圖,也就是說(注:不翻譯):

Every time we create dynamic component angular creates root view with root data, that contains references to elInjector and ngModule injector.

function createRootData( elInjector: Injector, ngModule: NgModuleRef<any>, rendererFactory: RendererFactory2, projectableNodes: any[][], rootSelectorOrNode: any): RootData {
  const sanitizer = ngModule.injector.get(Sanitizer);
  const errorHandler = ngModule.injector.get(ErrorHandler);
  const renderer = rendererFactory.createRenderer(null, null);
  return {
    ngModule,
    injector: elInjector, projectableNodes,
    selectorOrNode: rootSelectorOrNode, sanitizer, rendererFactory, renderer, errorHandler
  };
}
複製代碼

假設如今正在運行一個 Angular 程序。

下面代碼執行時,其內部發生了什麼:

platformBrowserDynamic().bootstrapModule(AppModule);
複製代碼

事實上,其內部發生了不少事情,可是咱們僅僅對 Angular 是 如何建立入口組件 這塊感興趣:

const compRef = componentFactory.create(Injector.NULL, [], selectorOrNode, ngModule);
複製代碼

Angular 注入樹就是從這裏開始,分叉爲兩顆並行樹。

Element Injector vs Module Injector

不久前,當延遲加載模塊被普遍使用時,在 github 上 有人報告了一個奇怪的案例:依賴注入系統會兩次實例化延遲加載模塊。結果,一個新的設計被引入。因此,從那開始,Angular 有兩個並行注入樹:元素注入樹和模塊注入樹。

主要規則是:當組件或指令須要解析依賴時,Angular 使用 Merge Injector 來遍歷 element injector tree,若是沒找到該依賴,則遍歷 module injector tree 去查找依賴。

Please note I don't use phrase "component injector" but rather "element injector".

什麼是 Merge Injector?

你之前可能寫過以下相似代碼:

@Directive({
  selector: '[someDir]'
}
export class SomeDirective {
 constructor(private injector: Injector) {}
}
複製代碼

這裏的 injector 就是 Merge Injector,固然你也能夠在組件中注入這個 Merge Injector

Merge Injector 對象的定義以下:

class Injector_ implements Injector {
  constructor(private view: ViewData, private elDef: NodeDef|null) {}
  get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any {
    const allowPrivateServices =
        this.elDef ? (this.elDef.flags & NodeFlags.ComponentView) !== 0 : false;
    return Services.resolveDep(
        this.view, this.elDef, allowPrivateServices,
        {flags: DepFlags.None, token, tokenKey: tokenKey(token)}, notFoundValue);
  }
}
複製代碼

如上代碼顯示了 Merge Injector 僅僅是視圖和元素的組合,這個注入器充當依賴解析時 element injector treemodule injector tree 之間的橋樑。

Merge Injector 也能夠解析內置的對象,如 ElementRefViewContainerRefTemplateRefChangeDetectorRef 等等,更有趣的是,它還能夠返回 Merge Injector

基本上每個 DOM 元素都有一個 merge injector,即便沒有提供任何令牌。

Tip: to get merge injector just open console and write:

ng.probe($0).injector
複製代碼

merge injector

可是你可能會問 element injector 是什麼?

咱們知道 @angular/compiler 會編譯組件模板生成工廠函數,該函數實際上只是調用 viewDef() 函數返回 ViewDefinition 類型對象,視圖僅僅是模板的一種表現形式,裏面包含各類類型節點,如 directivetextproviderquery 等等。其中有元素節點 element node 用來表示 DOM 元素的。實際上,元素注入器 element injector 就在這個節點內。Angular 會把該元素節點上全部的服務提供商都存儲在該節點的兩個屬性裏:

export interface ElementDef {
  ...
  /** * visible public providers for DI in the view, * as see from this element. This does not include private providers. */
  publicProviders: {[tokenKey: string]: NodeDef}|null;
  /** * same as visiblePublicProviders, but also includes private providers * that are located on this element. */
  allProviders: {[tokenKey: string]: NodeDef}|null;
}
複製代碼

讓咱們看看 元素注入器是如何解析依賴的

const providerDef =
  (allowPrivateServices ? elDef.element!.allProviders :
    elDef.element!.publicProviders)![tokenKey];
if (providerDef) {
  let providerData = asProviderData(searchView, providerDef.nodeIndex);
  if (!providerData) {
    providerData = { instance: _createProviderInstance(searchView, providerDef) };
    searchView.nodes[providerDef.nodeIndex] = providerData as any;
  }
  return providerData.instance;
}
複製代碼

這裏僅僅檢查 allProviders 屬性,或依據私有性檢查 publicProviders

這個注入器包含組件/指令對象,和其中的全部服務提供商。

視圖實例化階段 時主要由 ProviderElementContext 對象提供這些服務提供商,該對象也是 @angular/compiler Angular 編譯器的一部分。若是咱們深刻挖掘這個對象,會發現一些有趣的事情。

好比說,當使用 @Host 裝飾器時會有一些 限制,可使用宿主元素的 viewProviders 屬性來解決這些限制,能夠查看 medium.com/@a.yurich.z…

另外一個有趣的事情是,若是組件宿主元素上掛載指令,但組件和指令提供相同的令牌,則指令的服務提供商會 勝出

Tip: to get element injector just open console and write:

ng.probe($0).injector.elDef.element
複製代碼

element injector

依賴解析算法

視圖內依賴解析算法代碼是 resolveDep() 函數merge injectorget() 方法中也是使用這個函數來解析依賴(Services.resolveDep)。爲了理解依賴解析算法,咱們首先須要知道視圖和父視圖元素概念。

若是根組件有模板 ,咱們就會有三個視圖:

HostView_AppComponent
    <my-app></my-app>
View_AppComponent
    <child></child>
View_ChildComponent
    some content
複製代碼

依賴解析算法會根據多級視圖來解析:

dependecy resolution

若是子組件須要解析依賴,那 Angular 會首先查找該組件的元素注入器,也就是檢查 elRef.element.allProviders|publicProviders,而後 向上遍歷父視圖元素 檢查元素注入器的服務提供商(1),直到父視圖元素等於 null(2), 則返回 startView(3),而後檢查 startView.rootData.elnjector(4),最後,只有當令牌沒找到,再去檢查 startView.rootData module.injector(5)。(注:元素注入器 -> 組件注入器 -> 模塊注入器)

當向上遍歷組件視圖來解析依賴時,會搜索 視圖的父元素而不是元素的父元素。Angular 使用 viewParentEl() 函數獲取視圖父元素:

/** * for component views, this is the host element. * for embedded views, this is the index of the parent node * that contains the view container. */
export function viewParentEl(view: ViewData): NodeDef|null {
  const parentView = view.parent;
  if (parentView) {
    return view.parentNodeDef !.parent;
  } else {
    return null;
  }
}
複製代碼

好比說,假設有以下的一段小程序:

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

@Component({
  selector: 'my-list',
  template: `
    <div class="container">
      <grid-list>
        <grid-tile>1</grid-tile>
        <grid-tile>2</grid-tile>
        <grid-tile>3</grid-tile>
      </grid-list>
    </div>
  `
})
export class MyListComponent {}

@Component({
  selector: 'grid-list',
  template: `<ng-content></ng-content>`
})
export class GridListComponent {}

@Component({
  selector: 'grid-tile',
  template: `...`
})
export class GridTileComponent {
  constructor(private gridList: GridListComponent) {}
}
複製代碼

假設 grid-tile 組件依賴 GridListComponent,咱們能夠成功拿到該組件對象。可是這是怎麼作到的?

這裏父視圖元素到底是什麼?

下面的步驟回答了這個問題:

  1. 查找 起始元素GridListComponent 組件模板裏包含 grid-tile 元素選擇器,所以須要找到匹配 grid-tile 選擇器的元素。因此起始元素就是 grid-tile 元素。
  2. 查找擁有 grid-tile 元素的 模板,也就是 MyListComponent 組件模板。
  3. 決定該元素的視圖。若是沒有父嵌入視圖,則爲組件視圖,不然爲嵌入視圖。grid-tile 元素之上沒有任何 ng-template*structuralDirective,因此這裏是組件視圖 View_MyListComponent
  4. 查找視圖的父元素。這裏是視圖的父元素,而不是元素的父元素。

這裏有兩種狀況:

  • 對於嵌入視圖,父元素則爲包含該嵌入視圖的視圖容器。

好比,假設 grid-list 上掛載有結構指令:

@Component({
  selector: 'my-list',
  template: ` <div class="container"> <grid-list *ngIf="1"> <grid-tile>1</grid-tile> <grid-tile>2</grid-tile> <grid-tile>3</grid-tile> </grid-list> </div> `
})
export class MyListComponent {}
複製代碼

grid-tile 視圖的父元素則是 div.container

  • 對於組件視圖,父元素則爲宿主元素。

咱們上面的小程序也就是組件視圖,因此父視圖元素是 my-list 元素,而不是 grid-list

如今,你可能想知道若是 Angular 跳過 grid-list,則它是怎麼解析 GridListComponent 依賴的?

關鍵是 Angular 使用 原型鏈繼承 來蒐集服務提供商:

每次咱們爲一個元素提供服務提供商時,Angular 會新建立繼承於父節點的 allProviderspublicProviders 數組,不然不會新建立,僅僅會共享父節點的這兩個數組。

這就表示了 grid-tile 包含當前視圖內全部父元素的全部服務提供商。

下圖基本說明了 Angular 是如何爲模板內元素收集服務提供商:

collect providers

正如上圖顯示的,grid-tile 使用元素注入器經過 allProviders 成功拿到 GridListComponent 依賴,由於 grid-tile 元素注入器包含來自於父元素的服務提供商。

element injector allProviders

想要了解更多,能夠查看 StackOverflow answer

元素注入器的服務提供商使用了原型鏈繼承,致使咱們不能使用 multi 選項來提供同一令牌多個服務。可是因爲依賴注入系統很靈活,也有辦法去解決這個問題,能夠查看 stackoverflow.com/questions/4…

能夠把上文的解釋裝入腦中,如今繼續畫注入樹。

Simple my-app->child->grand-child application

假設有以下簡單程序:

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

@Component({
  selector: 'child',
  template: `<grand-child></grand-child>`
})
export class ChildComponent {}

@Component({
  selector: 'grand-child',
  template: `grand-child`
})
export class GrandChildComponent {
  constructor(private service: Service) {}
}

@NgModule({
  imports: [BrowserModule],
  declarations: [
    AppComponent, 
    ChildComponent, 
    GrandChildComponent
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
複製代碼

咱們有三層樹結構,而且 GrandChildComponent 依賴於 Service

my-app
   child
      grand-child(ask for Service dependency)
複製代碼

下圖解釋了 Angular 內部是如何解析 Service 依賴的:

angular resolve dependency

上圖從 View_Child (1)的 grand-child 元素開始,並向上遍歷查找全部視圖的父元素,當視圖沒有父元素時,本實例中 may-app 沒有父元素,則 使用根視圖的注入器查找(2):

startView.root.injector.get(depDef.token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR);
複製代碼

本實例中 startView.root.injector 就是 NullInjector,因爲 NullInjector 沒有任何服務提供商,則 Angular 就會 切換到模塊注入器(3):

startView.root.ngModule.injector.get(depDef.token, notFoundValue);
複製代碼

因此 Angular 會按照如下順序解析依賴:

AppModule Injector 
        ||
        \/
    ZoneInjector 
        ||
        \/
  Platform Injector 
        ||
        \/
    NullInjector 
        ||
        \/
       Error
複製代碼

路由程序

讓咱們修改程序,添加路由器:

@Component({
  selector: 'my-app',
  template: `<router-outlet></router-outlet>`,
})
export class AppComponent {}
...
@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      { path: 'child', component: ChildComponent },
      { path: '', redirectTo: '/child', pathMatch: 'full' }
    ])
  ],
  declarations: [
    AppComponent,
    ChildComponent,
    GrandChildComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }
複製代碼

這樣視圖樹就相似爲:

my-app
   router-outlet
   child
      grand-child(dynamic creation)
複製代碼

如今讓咱們看看 路由是如何建立動態組件的

const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector);                           
this.activated = this.location.createComponent(factory, this.location.length, injector);
複製代碼

這裏 Angular 使用新的 rootData 對象建立一個新的根視圖,同時傳入 OutletInjector 做爲根元素注入器 elInjectorOutletInjector 又依賴於父注入器 this.location.injector,該父注入器是 router-outlet 的元素注入器。

OutletInjector 是一種特別的注入器,行爲有些像路由組件和父元素 router-outlet 之間的橋樑,該對象代碼能夠看 這裏

OutletInjector

延遲加載程序

最後,讓咱們把 GrandChildComponent 移到延遲加載模塊,爲此須要在子組件中添加 router-outlet,並修改路由配置:

@Component({
  selector: 'child',
  template: ` Child <router-outlet></router-outlet> `
})
export class ChildComponent {}
...
@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      {
        path: 'child', component: ChildComponent,
        children: [
          { 
             path: 'grand-child', 
             loadChildren: './grand-child/grand-child.module#GrandChildModule'}
        ]
      },
      { path: '', redirectTo: '/child', pathMatch: 'full' }
    ])
  ],
  declarations: [
    AppComponent,
    ChildComponent
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}
複製代碼
my-app
   router-outlet
   child (dynamic creation)
       router-outlet
         +grand-child(lazy loading)
複製代碼

讓咱們爲延遲加載程序畫兩顆獨立的樹:

injector with lazy loading

Tree-shakeable tokens are on horizon

Angular 團隊爲讓框架變得更小,後續又作了大量工做,從 version 6 開始,提供了另外一種註冊服務提供商的方式。

Injectable

以前由 Injectable 裝飾的類不能說明它是否有依賴,與它如何被使用也無關。因此,若是一個服務沒有依賴,那 Injectable 裝飾器是能夠被移除的。

隨着 API 變得穩定,能夠配置 Injectable 裝飾器來告訴 Angular,該服務是屬於哪個模塊的,以及它是被如何實例化的:

export interface InjectableDecorator {
  (): any;
  (options?: {providedIn: Type<any>| 'root' | null}&InjectableProvider): any;
  new (): Injectable;
  new (options?: {providedIn: Type<any>| 'root' | null}&InjectableProvider): Injectable;
}

export type InjectableProvider = ValueSansProvider | ExistingSansProvider |
StaticClassSansProvider | ConstructorSansProvider | FactorySansProvider | ClassSansProvider;
複製代碼

下面是一個簡單實用案例:

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

@Injectable({
  providedIn: 'root',
  useClass: MyService,
  deps: []
})
export class AnotherService {}
複製代碼

ngModule factory 包含全部服務提供商不一樣,這裏把有關服務提供商的信息存儲在 Injectable 裝飾器內。這個技術會讓咱們程序代碼變得更小,由於沒有被使用的服務會被搖樹優化掉。若是咱們使用 Injectable 來註冊服務提供商,而使用者又不導入咱們的服務提供商,那最後被打包的代碼不包含這些服務提供商,因此,

Prefer registering providers in Injectables over NgModule.providers over Component.providers

本文開始時我提到過根注入器的 Modules Tokens,因此 Angular 可以區分哪個模塊出如今特定的模塊注入器內。

依賴解析器會使用這個信息來 判斷可搖樹優化令牌是否屬於模塊注入器

InjectionToken

可使用 InjectionToken 對象來定義依賴注入系統如何構造一個令牌以及該令牌應用於哪個注入器:

export class InjectionToken<T> {
  constructor(protected _desc: string, options?: {
    providedIn?: Type<any>| 'root' | null,
    factory: () => T
  }) {}
}
複製代碼

因此應該這樣使用:

export const apiUrl = new InjectionToken('tree-shakeable apiUrl token', {                                   
  providedIn: 'root',                               
  factory: () => 'someUrl'
});
複製代碼

結論

依賴注入是 Angular 框架中的一個很是複雜的話題,知道其內部工做原理會讓你對你作的事情更有信心,因此我強烈建議偶爾去深刻研究 Angular 源代碼。

注:這篇文章頗有深度,很長也很難,加油吧!

相關文章
相關標籤/搜索