本篇文章帶大家進(jìn)行Angular源碼學(xué)習(xí),介紹一下多級依賴注入設(shè)計,希望對大家有所幫助!
作為“為大型前端項目”而設(shè)計的前端框架,Angular 其實(shí)有許多值得參考和學(xué)習(xí)的設(shè)計,本系列主要用于研究這些設(shè)計和功能的實(shí)現(xiàn)原理。本文主要圍繞 Angular 中的最大特點(diǎn)——依賴注入,介紹 Angular 中多級依賴注入的設(shè)計?!鞠嚓P(guān)教程推薦:《angular教程》】
上一篇我們介紹了 Angular 中的Injectot
注入器、Provider
提供者,以及注入器機(jī)制。那么,在 Angular 應(yīng)用中,各個組件和模塊間又是怎樣共享依賴的,同樣的服務(wù)是否可以多次實(shí)例化呢?
組件和模塊的依賴注入過程,離不開 Angular 多級依賴注入的設(shè)計,我們來看看。
多級依賴注入
前面我們說過,Angular 中的注入器是可繼承、且分層的。
在 Angular 中,有兩個注入器層次結(jié)構(gòu):
ModuleInjector
模塊注入器:使用@NgModule()
或@Injectable()
注解在此層次結(jié)構(gòu)中配置ModuleInjector
ElementInjector
元素注入器:在每個 DOM 元素上隱式創(chuàng)建
模塊注入器和元素注入器都是樹狀結(jié)構(gòu)的,但它們的分層結(jié)構(gòu)并不完全一致。
模塊注入器
模塊注入器的分層結(jié)構(gòu),除了與應(yīng)用中模塊設(shè)計有關(guān)系,還有平臺模塊(PlatformModule)注入器與應(yīng)用程序模塊(AppModule)注入器的分層結(jié)構(gòu)。
平臺模塊(PlatformModule)注入器
在 Angular 術(shù)語中,平臺是供 Angular 應(yīng)用程序在其中運(yùn)行的上下文。Angular 應(yīng)用程序最常見的平臺是 Web 瀏覽器,但它也可以是移動設(shè)備的操作系統(tǒng)或 Web 服務(wù)器。
Angular 應(yīng)用在啟動時,會創(chuàng)建一個平臺層:
- 平臺是 Angular 在網(wǎng)頁上的入口點(diǎn),每個頁面只有一個平臺
- 頁面上運(yùn)行的每個 Angular 應(yīng)用程序,所共有的服務(wù)都在平臺內(nèi)綁定
一個 Angular 平臺,主要包括創(chuàng)建模塊實(shí)例、銷毀等功能:
@Injectable() export class PlatformRef { // 傳入注入器,作為平臺注入器 constructor(private _injector: Injector) {} // 為給定的平臺創(chuàng)建一個 @NgModule 的實(shí)例,以進(jìn)行離線編譯 bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options?: BootstrapOptions): Promise<NgModuleRef<M>> {} // 使用給定的運(yùn)行時編譯器,為給定的平臺創(chuàng)建一個 @NgModule 的實(shí)例 bootstrapModule<M>( moduleType: Type<M>, compilerOptions: (CompilerOptions&BootstrapOptions)| Array<CompilerOptions&BootstrapOptions> = []): Promise<NgModuleRef<M>> {} // 注冊銷毀平臺時要調(diào)用的偵聽器 onDestroy(callback: () => void): void {} // 獲取平臺注入器 // 該平臺注入器是頁面上每個 Angular 應(yīng)用程序的父注入器,并提供單例提供程序 get injector(): Injector {} // 銷毀頁面上的當(dāng)前 Angular 平臺和所有 Angular 應(yīng)用程序,包括銷毀在平臺上注冊的所有模塊和偵聽器 destroy() {} }
實(shí)際上,平臺在啟動的時候(bootstrapModuleFactory
方法中),在ngZone.run
中創(chuàng)建ngZoneInjector
,以便在 Angular 區(qū)域中創(chuàng)建所有實(shí)例化的服務(wù),而ApplicationRef
(頁面上運(yùn)行的 Angular 應(yīng)用程序)將在 Angular 區(qū)域之外創(chuàng)建。
在瀏覽器中啟動時,會創(chuàng)建瀏覽器平臺:
export const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef = createPlatformFactory(platformCore, 'browser', INTERNAL_BROWSER_PLATFORM_PROVIDERS); // 其中,platformCore 平臺必須包含在任何其他平臺中 export const platformCore = createPlatformFactory(null, 'core', _CORE_PLATFORM_PROVIDERS);
使用平臺工廠(例如上面的createPlatformFactory
)創(chuàng)建平臺時,將隱式初始化頁面的平臺:
export function createPlatformFactory( parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef)|null, name: string, providers: StaticProvider[] = []): (extraProviders?: StaticProvider[]) => PlatformRef { const desc = `Platform: ${name}`; const marker = new InjectionToken(desc); // DI 令牌 return (extraProviders: StaticProvider[] = []) => { let platform = getPlatform(); // 若平臺已創(chuàng)建,則不做處理 if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) { if (parentPlatformFactory) { // 若有父級平臺,則直接使用父級平臺,并更新相應(yīng)的提供者 parentPlatformFactory( providers.concat(extraProviders).concat({provide: marker, useValue: true})); } else { const injectedProviders: StaticProvider[] = providers.concat(extraProviders).concat({provide: marker, useValue: true}, { provide: INJECTOR_SCOPE, useValue: 'platform' }); // 若無父級平臺,則新建注入器,并創(chuàng)建平臺 createPlatform(Injector.create({providers: injectedProviders, name: desc})); } } return assertPlatform(marker); }; }
通過以上過程,我們知道 Angular 應(yīng)用在創(chuàng)建平臺的時候,創(chuàng)建平臺的模塊注入器ModuleInjector
。我們從上一節(jié)Injector
定義中也能看到,NullInjector
是所有注入器的頂部:
export abstract class Injector { static NULL: Injector = new NullInjector(); }
因此,在平臺模塊注入器之上,還有NullInjector()
。而在平臺模塊注入器之下,則還有應(yīng)用程序模塊注入器。
應(yīng)用程序根模塊(AppModule)注入器
每個應(yīng)用程序有至少一個 Angular 模塊,根模塊就是用來啟動此應(yīng)用的模塊:
@NgModule({ providers: APPLICATION_MODULE_PROVIDERS }) export class ApplicationModule { // ApplicationRef 需要引導(dǎo)程序提供組件 constructor(appRef: ApplicationRef) {} }
AppModule
根應(yīng)用模塊由BrowserModule
重新導(dǎo)出,當(dāng)我們使用 CLI 的new
命令創(chuàng)建新應(yīng)用時,它會自動包含在根AppModule
中。應(yīng)用程序根模塊中,提供者關(guān)聯(lián)著內(nèi)置的 DI 令牌,用于為引導(dǎo)程序配置根注入器。
Angular 還將ComponentFactoryResolver
添加到根模塊注入器中。此解析器存儲了entryComponents
系列工廠,因此它負(fù)責(zé)動態(tài)創(chuàng)建組件。
模塊注入器層級
到這里,我們可以簡單地梳理出模塊注入器的層級關(guān)系:
-
模塊注入器樹的最上層則是應(yīng)用程序根模塊(AppModule)注入器,稱作 root。
-
在 root 之上還有兩個注入器,一個是平臺模塊(PlatformModule)注入器,一個是
NullInjector()
。
因此,模塊注入器的分層結(jié)構(gòu)如下:
在我們實(shí)際的應(yīng)用中,它很可能是這樣的:
Angular DI 具有分層注入體系,這意味著下級注入器也可以創(chuàng)建它們自己的服務(wù)實(shí)例。
元素注入器
前面說過,在 Angular 中有兩個注入器層次結(jié)構(gòu),分別是模塊注入器和元素注入器。
元素注入器的引入
當(dāng) Angular 中懶加載的模塊開始廣泛使用時,出現(xiàn)了一個 issue:依賴注入系統(tǒng)導(dǎo)致懶加載模塊的實(shí)例化加倍。
在這一次修復(fù)中,引入了新的設(shè)計:注入器使用兩棵并行的樹,一棵用于元素,另一棵用于模塊。
Angular 會為所有entryComponents
創(chuàng)建宿主工廠,它們是所有其他組件的根視圖。
這意味著每次我們創(chuàng)建動態(tài) Angular 組件時,都會使用根數(shù)據(jù)(RootData
)創(chuàng)建根視圖(RootView
):
class ComponentFactory_ extends ComponentFactory<any>{ create( injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string|any, ngModule?: NgModuleRef<any>): ComponentRef<any> { if (!ngModule) { throw new Error('ngModule should be provided'); } const viewDef = resolveDefinition(this.viewDefFactory); const componentNodeIndex = viewDef.nodes[0].element!.componentProvider!.nodeIndex; // 使用根數(shù)據(jù)創(chuàng)建根視圖 const view = Services.createRootView( injector, projectableNodes || [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT); // view.nodes 的訪問器 const component = asProviderData(view, componentNodeIndex).instance; if (rootSelectorOrNode) { view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full); } // 創(chuàng)建組件 return new ComponentRef_(view, new ViewRef_(view), component); } }
該根數(shù)據(jù)(RootData
)包含對elInjector
和ngModule
注入器的引用:
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, }; }
引入元素注入器樹,原因是這樣的設(shè)計比較簡單。通過更改注入器層次結(jié)構(gòu),避免交錯插入模塊和組件注入器,從而導(dǎo)致延遲加載模塊的雙倍實(shí)例化。因為每個注入器都只有一個父對象,并且每次解析都必須精確地尋找一個注入器來檢索依賴項。
元素注入器(Element Injector)
在 Angular 中,視圖是模板的表示形式,它包含不同類型的節(jié)點(diǎn),其中便有元素節(jié)點(diǎn),元素注入器位于此節(jié)點(diǎn)上:
export interface ElementDef { ... // 在該視圖中可見的 DI 的公共提供者 publicProviders: {[tokenKey: string]: NodeDef}|null; // 與 visiblePublicProviders 相同,但還包括位于此元素上的私有提供者 allProviders: {[tokenKey: string]: NodeDef}|null; }
默認(rèn)情況下ElementInjector
為空,除非在@Directive()
或@Component()
的providers
屬性中進(jìn)行配置。
當(dāng) Angular 為嵌套的 HTML 元素創(chuàng)建元素注入器時,要么從父元素注入器繼承它,要么直接將父元素注入器分配給子節(jié)點(diǎn)定義。
如果子 HTML 元素上的元素注入器具有提供者,則應(yīng)該繼承該注入器。否則,無需為子組件創(chuàng)建單獨(dú)的注入器,并且如果需要,可以直接從父級的注入器中解決依賴項。
元素注入器與模塊注入器的設(shè)計
那么,元素注入器與模塊注入器是從哪個地方開始成為平行樹的呢?
我們已經(jīng)知道,應(yīng)用程序根模塊(AppModule
)會在使用 CLI 的new
命令創(chuàng)建新應(yīng)用時,自動包含在根AppModule
中。
當(dāng)應(yīng)用程序(ApplicationRef
)啟動(bootstrap
)時,會創(chuàng)建entryComponent
:
const compRef = componentFactory.create(Injector.NULL, [], selectorOrNode, ngModule);
該過程會使用根數(shù)據(jù)(RootData
)創(chuàng)建根視圖(RootView
),同時會創(chuàng)建根元素注入器,在這里elInjector
為Injector.NULL
。
在這里,Angular 的注入器樹被分成元素注入器樹和模塊注入器樹,這兩個平行的樹了。
Angular 會有規(guī)律的創(chuàng)建下級注入器,每當(dāng) Angular 創(chuàng)建一個在@Component()
中指定了providers
的組件實(shí)例時,它也會為該實(shí)例創(chuàng)建一個新的子注入器。類似的,當(dāng)在運(yùn)行期間加載一個新的NgModule
時,Angular 也可以為它創(chuàng)建一個擁有自己的提供者的注入器。
子模塊和組件注入器彼此獨(dú)立,并且會為所提供的服務(wù)分別創(chuàng)建自己的實(shí)例。當(dāng) Angular 銷毀NgModule
或組件實(shí)例時,也會銷毀這些注入器以及注入器中的那些服務(wù)實(shí)例。
Angular 解析依賴過程
上面我們介紹了 Angular 中的兩種注入器樹:模塊注入器樹和元素注入器樹。那么,Angular 在提供依賴時,又會以怎樣的方式去進(jìn)行解析呢。
在 Angular 種,當(dāng)為組件/指令解析 token 獲取依賴時,Angular 分為兩個階段來解析它:
- 針對
ElementInjector
層次結(jié)構(gòu)(其父級) - 針對
ModuleInjector
層次結(jié)構(gòu)(其父級)
其過程如下(參考多級注入器-解析規(guī)則):
-
當(dāng)組件聲明依賴項時,Angular 會嘗試使用它自己的
ElementInjector
來滿足該依賴。 -
如果組件的注入器缺少提供者,它將把請求傳給其父組件的
ElementInjector
。 -
這些請求將繼續(xù)轉(zhuǎn)發(fā),直到 Angular 找到可以處理該請求的注入器或用完祖先
ElementInjector
。 -
如果 Angular 在任何
ElementInjector
中都找不到提供者,它將返回到發(fā)起請求的元素,并在ModuleInjector
層次結(jié)構(gòu)中進(jìn)行查找。 -
如果 Angular 仍然找不到提供者,它將引發(fā)錯誤。
為此,Angular 引入一種特殊的合并注入器。
合并注入器(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); } }
當(dāng) Angular 解析依賴項時,合并注入器則是元素注入器樹和模塊注入器樹之間的橋梁。當(dāng) Angular 嘗試解析組件或指令中的某些依賴關(guān)系時,會使用合并注入器來遍歷元素注入器樹,然后,如果找不到依賴關(guān)系,則切換到模塊注入器樹以解決依賴關(guān)系。
class ViewContainerRef_ implements ViewContainerData { ... // 父級試圖元素注入器的查詢 get parentInjector(): Injector { let view = this._view; let elDef = this._elDef.parent; while (!elDef && view) { elDef = viewParentEl(view); view = view.parent!; } return view ? new Injector_(view, elDef) : new Injector_(this._view, null); } }
解析過程
注入器是可繼承的,這意味著如果指定的注入器無法解析某個依賴,它就會請求父注入器來解析它。具體的解析算法在resolveDep()
方法中實(shí)現(xiàn):
export function resolveDep( view: ViewData, elDef: NodeDef, allowPrivateServices: boolean, depDef: DepDef, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any { // // mod1 // / // el1 mod2 // / // el2 // // 請求 el2.injector.get(token)時,按以下順序檢查并返回找到的第一個值: // - el2.injector.get(token, default) // - el1.injector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) -> do not check the module // - mod2.injector.get(token, default) }
如果是<child></child>
這樣模板的根AppComponent
組件,那么在 Angular 中將具有三個視圖:
<!-- HostView_AppComponent --> <my-app></my-app> <!-- View_AppComponent --> <child></child> <!-- View_ChildComponent --> some content
依賴解析過程,解析算法會基于視圖層次結(jié)構(gòu),如圖所示進(jìn)行:
如果在子組件中解析某些令牌,Angular 將:
-
首先查看子元素注入器,進(jìn)行檢查
elRef.element.allProviders|publicProviders
。 -
然后遍歷所有父視圖元素(1),并檢查元素注入器中的提供者。
-
如果下一個父視圖元素等于
null
(2),則返回到startView
(3),檢查startView.rootData.elnjector
(4)。 -
只有在找不到令牌的情況下,才檢查
startView.rootData module.injector
( 5 )。
由此可見,Angular 在遍歷組件以解析某些依賴性時,將搜索特定視圖的父元素而不是特定元素的父元素。視圖的父元素可以通過以下方法獲得:
// 對于組件視圖,這是宿主元素 // 對于嵌入式視圖,這是包含視圖容器的父節(jié)點(diǎn)的索引 export function viewParentEl(view: ViewData): NodeDef|null { const parentView = view.parent; if (parentView) { return view.parentNodeDef !.parent; } else { return null; } }
總結(jié)
本文主要介紹了 Angular 中注入器的層級結(jié)構(gòu),在 Angular 中有兩棵平行的注入器樹:模塊注入器樹和元素注入器樹。
元素注入器樹的引入,主要是為了解決依賴注入解析懶加載模塊時,導(dǎo)致模塊的雙倍實(shí)例化問題。在元素注入器樹引入后,Angular 解析依賴的過程也有調(diào)整,優(yōu)先尋找元素注入器以及父視圖元素注入器等注入器的依賴,只有元素注入器中無法找到令牌時,才會查詢模塊注入器中的依賴。