成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

[譯] 關于 Angular 依賴注入你需要知道的

Edison / 3183人閱讀

摘要:注入器樹大多數(shù)開發(fā)者知道,會創(chuàng)建根注入器,根注入器內(nèi)的服務都是單例的。也會被添加到這個根注入器對象內(nèi),它主要用來創(chuàng)建動態(tài)組件,因為它存儲了屬性指向的組件數(shù)組。為了理解依賴解析算法,我們首先需要知道視圖和父視圖元素概念。

What you always wanted to know about Angular Dependency Injection tree

如果你之前沒有深入了解 Angular 依賴注入系統(tǒng),那你現(xiàn)在可能認為 Angular 程序內(nèi)的根注入器包含所有合并的服務提供商,每一個組件都有它自己的注入器,延遲加載模塊有它自己的注入器。

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

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

所以現(xiàn)在去看看,可能有意外收獲嗷。

注入器樹(Injector Tree)

大多數(shù)開發(fā)者知道,Angular 會創(chuàng)建根注入器,根注入器內(nèi)的服務都是單例的。但是,貌似還有其他注入器是它的父級。

作為一名開發(fā)者,我想知道 Angular 是怎么構建注入器樹的,下圖是注入器樹的頂層部分:

這不是整棵樹,目前還沒有任何組件呢,后面會繼續(xù)畫樹的剩余部分。但是現(xiàn)在先看下根注入器 AppModule Injector,因為它是最常使用的。

根注入器(Root AppModule Injector)

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

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.

根據(jù)這個規(guī)則,上圖中 EagerModule2MyService2 也會被包含在根注入器 AppModule Injector 中。

ComponentFactoryResolver 也會被 Angular 添加 到這個根注入器對象內(nèi),它主要用來創(chuàng)建動態(tài)組件,因為它存儲了 entryComponents 屬性指向的組件數(shù)組。

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

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

我們可以看到這個工廠函數(shù)返回一個包含所有被合并服務提供商的模塊對象,所有開發(fā)者都應當熟悉這個(注:可以查看 譯 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

還有很多其他知識點,我在這里沒有描述,因為官網(wǎng)上已經(jīng)談到了:

https://angular.io/guide/ngmodule-faq

https://angular.io/guide/hierarchical-dependency-injection

Platform Injector

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

PlatformInjector 會在 PlatformRef 對象初始化的時候,包含內(nèi)置的服務提供商,但也可以額外包含服務提供商:

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

這些額外的服務提供商是由我們開發(fā)者傳入的,且必須是 StaticProviders。如果你不熟悉 StaticProvidersProvider 兩者間的區(qū)別,可以查看這個 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();

盡管根注入器以及其父注入器解析依賴的過程清晰明了,但是組件級別的注入器如何解析依賴卻讓我很困惑,所以,我接著去深入了解。

EntryComponent and RootData

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

Angular 會為所有入口組件創(chuàng)建宿主工廠函數(shù),這些宿主工廠函數(shù)就是其他視圖的根視圖,也就是說(注:不翻譯):

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, 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
  };
}

假設現(xiàn)在正在運行一個 Angular 程序。

下面代碼執(zhí)行時,其內(nèi)部發(fā)生了什么:

platformBrowserDynamic().bootstrapModule(AppModule);

事實上,其內(nèi)部發(fā)生了很多事情,但是我們僅僅對 Angular 是 如何創(chuàng)建入口組件 這塊感興趣:

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

Angular 注入樹就是從這里開始,分叉為兩顆并行樹。

Element Injector vs Module Injector

不久前,當延遲加載模塊被廣泛使用時,在 github 上 有人報告了一個奇怪的案例:依賴注入系統(tǒng)會兩次實例化延遲加載模塊。結果,一個新的設計被引入。所以,從那開始,Angular 有兩個并行注入樹:元素注入樹和模塊注入樹。

主要規(guī)則是:當組件或指令需要解析依賴時,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 也可以解析內(nèi)置的對象,如 ElementRef,ViewContainerRefTemplateRef,ChangeDetectorRef 等等,更有趣的是,它還可以返回 Merge Injector。

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

Tip: to get merge injector just open console and write:
ng.probe($0).injector

但是你可能會問 element injector 是什么?

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

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 屬性,或依據(jù)私有性檢查 publicProviders

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

視圖實例化階段 時主要由 ProviderElementContext 對象提供這些服務提供商,該對象也是 @angular/compiler Angular 編譯器的一部分。如果我們深入挖掘這個對象,會發(fā)現(xiàn)一些有趣的事情。

比如說,當使用 @Host 裝飾器時會有一些 限制,可以使用宿主元素的 viewProviders 屬性來解決這些限制,可以查看 https://medium.com/@a.yurich.... 。

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

Tip: to get element injector just open console and write:
ng.probe($0).injector.elDef.element

依賴解析算法

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

如果根組件有模板 ,我們就會有三個視圖:

HostView_AppComponent
    
View_AppComponent
    
View_ChildComponent
    some content

依賴解析算法會根據(jù)多級視圖來解析:

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

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

/**
 * 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: ``
})
export class AppComponent {}

@Component({
  selector: "my-list",
  template: `
    
1 2 3
` }) export class MyListComponent {} @Component({ selector: "grid-list", template: `` }) export class GridListComponent {} @Component({ selector: "grid-tile", template: `...` }) export class GridTileComponent { constructor(private gridList: GridListComponent) {} }

假設 grid-tile 組件依賴 GridListComponent,我們可以成功拿到該組件對象。但是這是怎么做到的?

這里父視圖元素究竟是什么?

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

查找 起始元素。GridListComponent 組件模板里包含 grid-tile 元素選擇器,因此需要找到匹配 grid-tile 選擇器的元素。所以起始元素就是 grid-tile 元素。

查找擁有 grid-tile 元素的 模板,也就是 MyListComponent 組件模板。

決定該元素的視圖。如果沒有父嵌入視圖,則為組件視圖,否則為嵌入視圖。grid-tile 元素之上沒有任何 ng-template*structuralDirective,所以這里是組件視圖 View_MyListComponent。

查找視圖的父元素。這里是視圖的父元素,而不是元素的父元素。

這里有兩種情況:

對于嵌入視圖,父元素則為包含該嵌入視圖的視圖容器。

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

@Component({
  selector: "my-list",
  template: `
    
1 2 3
` }) export class MyListComponent {}

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

對于組件視圖,父元素則為宿主元素。

我們上面的小程序也就是組件視圖,所以父視圖元素是 my-list 元素,而不是 grid-list。

現(xiàn)在,你可能想知道如果 Angular 跳過 grid-list,則它是怎么解析 GridListComponent 依賴的?

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

每次我們?yōu)橐粋€元素提供服務提供商時,Angular 會新創(chuàng)建繼承于父節(jié)點的 allProviderspublicProviders 數(shù)組,否則不會新創(chuàng)建,僅僅會共享父節(jié)點的這兩個數(shù)組。

這就表示了 grid-tile 包含當前視圖內(nèi)所有父元素的所有服務提供商。

下圖基本說明了 Angular 是如何為模板內(nèi)元素收集服務提供商:

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

想要了解更多,可以查看 StackOverflow answer。

元素注入器的服務提供商使用了原型鏈繼承,導致我們不能使用 multi 選項來提供同一令牌多個服務。但是由于依賴注入系統(tǒng)很靈活,也有辦法去解決這個問題,可以查看 https://stackoverflow.com/que...。

可以把上文的解釋裝入腦中,現(xiàn)在繼續(xù)畫注入樹。

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

假設有如下簡單程序:

@Component({
  selector: "my-app",
  template: ``,
})
export class AppComponent {}

@Component({
  selector: "child",
  template: ``
})
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 內(nèi)部是如何解析 Service 依賴的:

上圖從 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: ``,
})
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)

現(xiàn)在讓我們看看 路由是如何創(chuàng)建動態(tài)組件的

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

這里 Angular 使用新的 rootData 對象創(chuàng)建一個新的根視圖,同時傳入 OutletInjector 作為根元素注入器 elInjectorOutletInjector 又依賴于父注入器 this.location.injector,該父注入器是 router-outlet 的元素注入器。

OutletInjector 是一種特別的注入器,行為有些像路由組件和父元素 router-outlet 之間的橋梁,該對象代碼可以看 這里

延遲加載程序

最后,讓我們把 GrandChildComponent 移到延遲加載模塊,為此需要在子組件中添加 router-outlet,并修改路由配置:

@Component({
  selector: "child",
  template: `
    Child
    
  `
})
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)

讓我們?yōu)檠舆t加載程序畫兩顆獨立的樹:

Tree-shakeable tokens are on horizon

Angular 團隊為讓框架變得更小,后續(xù)又做了大量工作,從 version 6 開始,提供了另一種注冊服務提供商的方式。

Injectable

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

隨著 API 變得穩(wěn)定,可以配置 Injectable 裝飾器來告訴 Angular,該服務是屬于哪一個模塊的,以及它是被如何實例化的:

export interface InjectableDecorator {
  (): any;
  (options?: {providedIn: Type| "root" | null}&InjectableProvider): any;
  new (): Injectable;
  new (options?: {providedIn: Type| "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 裝飾器內(nèi)。這個技術會讓我們程序代碼變得更小,因為沒有被使用的服務會被搖樹優(yōu)化掉。如果我們使用 Injectable 來注冊服務提供商,而使用者又不導入我們的服務提供商,那最后被打包的代碼不包含這些服務提供商,所以,

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

本文開始時我提到過根注入器的 Modules Tokens,所以 Angular 能夠區(qū)分哪一個模塊出現(xiàn)在特定的模塊注入器內(nèi)。

依賴解析器會使用這個信息來 判斷可搖樹優(yōu)化令牌是否屬于模塊注入器。

InjectionToken

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

export class InjectionToken {
  constructor(protected _desc: string, options?: {
    providedIn?: Type| "root" | null,
    factory: () => T
  }) {}
}

所以應該這樣使用:

export const apiUrl = new InjectionToken("tree-shakeable apiUrl token", {                                   
  providedIn: "root",                               
  factory: () => "someUrl"
});
結論

依賴注入是 Angular 框架中的一個非常復雜的話題,知道其內(nèi)部工作原理會讓你對你做的事情更有信心,所以我強烈建議偶爾去深入研究 Angular 源代碼。

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

文章版權歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/108392.html

相關文章

  • [] Angular @Host 裝飾器和元素注入

    摘要:裝飾器我們?yōu)樯兑懻撛刈⑷肫鞫皇茄b飾器這是因為會把元素注入器依賴解析過程限制在當前組件視圖內(nèi)。但是一旦使用了裝飾器,整個依賴解析過程就會在第一階段完成后停止解析,也就是說,元素注入器只在組件視圖內(nèi)解析依賴,然后就停止解析工作。 原文鏈接:A curious case of the @Host decorator and Element Injectors in Angular 我...

    marek 評論0 收藏0
  • [] 別再對 Angular Modules 感到迷惑

    摘要:大多數(shù)初學者會認為也有封裝規(guī)則,但實際上沒有。第二個規(guī)則是最后導入模塊的,會覆蓋前面導入模塊的。 原文鏈接:Avoiding common confusions with modules in Angular showImg(https://segmentfault.com/img/remote/1460000015298243?w=270&h=360); Angular Modul...

    LMou 評論0 收藏0
  • [] 如何使用 TypeScript 編寫 AngularJS Controller?

    摘要:在這篇文章里,我將介紹如何使用去編寫的。一個新的子將被創(chuàng)建并作為變量注入到的構造函數(shù)當中。當使用一個構造函數(shù)就可以很好地解決問題,因為函數(shù)提升起到了很重要的作用。自定義接口類型接著就可以在構造器使用參數(shù)獲得強類型和智能支持了。 原文鏈接 : How to write AngularJS controller using TypeScript?原文作者 : Siddharth Pande...

    alphahans 評論0 收藏0
  • [] 關于 `ExpressionChangedAfterItHasBeenCheckedErro

    摘要:本文將解釋引起這個錯誤的內(nèi)在原因,檢測機制的內(nèi)部原理,提供導致這個錯誤的共同行為,并給出修復這個錯誤的解決方案。這一次過程稱為。這個程序設計為子組件拋出一個事件,而父組件監(jiān)聽這個事件,而這個事件會引起父組件屬性值發(fā)生改變。 原文鏈接:Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedE...

    andong777 評論0 收藏0
  • [] 關于 Angular 動態(tài)組件需要知道

    摘要:第一種方式是使用模塊加載器,如果你使用加載器的話,路由在加載子路由模塊時也是用的作為模塊加載器。還需注意的是,想要使用還需像這樣去注冊它你當然可以在里使用任何標識,不過路由模塊使用標識,所以最好也使用相同。 原文鏈接:Here is what you need to know about dynamic components in?Angular showImg(https://se...

    lcodecorex 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<