本篇文章帶大家進行Angular原始碼學習,介紹一下多層依賴注入設計,希望對大家有幫助!
作為「為大型前端專案」而設計的前端框架,Angular 其實有許多值得參考和學習的設計,本系列主要用於研究這些設計和功能的實現原理。本文主要圍繞在 Angular 中的最大特點──依賴注入,介紹 Angular 中多層依賴注入的設計。 【相關教學推薦:《angular教學》】
上一篇我們介紹了Angular 中的Injectot
注入器、Provider
提供者,以及注入器機制。那麼,在 Angular 應用中,各個元件和模組間又是怎樣共享依賴的,同樣的服務是否可以多次實例化呢?
元件和模組的依賴注入過程,離不開 Angular 多層依賴注入的設計,我們來看看。
多層依賴注入
前面我們說過,Angular 中的注入器是可繼承、且分層的。
在Angular 中,有兩個注入器層次結構:
-
#ModuleInjector
模組注入器:使用@NgModule()
或@Injectable()
註解在此層次結構中配置ModuleInjector
-
ElementInjector
元素注入器:在每個DOM 元素上隱含地創建
模組注入器和元素注入器都是樹狀結構的,但它們的分層結構並不完全一致。
模組注入器
模組注入器的分層結構,除了與應用程式中模組設計有關係,還有平台模組(PlatformModule)注入器與應用程式模組(AppModule)注入器的分層結構。
平台模組(PlatformModule)注入器
在 Angular 術語中,平台是供 Angular 應用程式在其中運行的上下文。 Angular 應用程式最常見的平台是 Web 瀏覽器,但它也可以是行動裝置的作業系統或 Web 伺服器。
Angular 應用程式在啟動時,會創建一個平台層:
- 平台是Angular 在網頁上的入口點,每個頁面只有一個平台
- 頁面上運行的每個Angular 應用程序,所共有的服務都在平台內綁定
一個Angular 平台,主要包括創建模組實例、銷毀等功能:
@Injectable() export class PlatformRef { // 传入注入器,作为平台注入器 constructor(private _injector: Injector) {} // 为给定的平台创建一个 @NgModule 的实例,以进行离线编译 bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options?: BootstrapOptions): Promise<NgModuleRef<M>> {} // 使用给定的运行时编译器,为给定的平台创建一个 @NgModule 的实例 bootstrapModule<M>( moduleType: Type<M>, compilerOptions: (CompilerOptions&BootstrapOptions)| Array<CompilerOptions&BootstrapOptions> = []): Promise<NgModuleRef<M>> {} // 注册销毁平台时要调用的侦听器 onDestroy(callback: () => void): void {} // 获取平台注入器 // 该平台注入器是页面上每个 Angular 应用程序的父注入器,并提供单例提供程序 get injector(): Injector {} // 销毁页面上的当前 Angular 平台和所有 Angular 应用程序,包括销毁在平台上注册的所有模块和侦听器 destroy() {} }
實際上,平台在啟動的時候(bootstrapModuleFactory
方法中),在ngZone.run
中創建ngZoneInjector
,以便在Angular 區域中創建所有實例化的服務,而ApplicationRef
(頁面上執行的Angular 應用程式)將在Angular 區域之外建立。
在瀏覽器中啟動時,會建立瀏覽器平台:
export const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef = createPlatformFactory(platformCore, 'browser', INTERNAL_BROWSER_PLATFORM_PROVIDERS); // 其中,platformCore 平台必须包含在任何其他平台中 export const platformCore = createPlatformFactory(null, 'core', _CORE_PLATFORM_PROVIDERS);
使用平台工廠(例如上面的createPlatformFactory
)建立平台時,將隱含初始化頁面的平台:
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(); // 若平台已创建,则不做处理 if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) { if (parentPlatformFactory) { // 若有父级平台,则直接使用父级平台,并更新相应的提供者 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' }); // 若无父级平台,则新建注入器,并创建平台 createPlatform(Injector.create({providers: injectedProviders, name: desc})); } } return assertPlatform(marker); }; }
透過以上過程,我們知道Angular 應用在創建平台的時候,創建平台的模組注入器ModuleInjector
。我們從上一節Injector
定義中也能看到,NullInjector
是所有註入器的頂端:
export abstract class Injector { static NULL: Injector = new NullInjector(); }
因此,在平台模組注入器之上,還有NullInjector()
。而在平台模組注入器之下,則還有應用程式模組注入器。
應用程式根模組(AppModule)注入器
每個應用程式至少有一個Angular 模組,根模組就是用來啟動此應用程式的模組:
@NgModule({ providers: APPLICATION_MODULE_PROVIDERS }) export class ApplicationModule { // ApplicationRef 需要引导程序提供组件 constructor(appRef: ApplicationRef) {} }
AppModule
根應用程式模組由BrowserModule
重新匯出,當我們使用CLI 的new
指令建立新應用程式時,它會自動包含在根AppModule
中。在應用程式根模組中,提供者關聯內建的 DI 令牌,用於為引導程式配置根注入器。
Angular 也將ComponentFactoryResolver
加入到根模組注入器中。此解析器儲存了entryComponents
系列工廠,因此它負責動態建立元件。
模組注入器層級
到這裡,我們可以簡單地梳理出模組注入器的層級關係:
模組注入器樹的最上層則是應用程式根模組(AppModule)注入器,稱為root。
在 root 之上還有兩個注入器,一個是平台模組(PlatformModule)注入器,一個是
NullInjector()
。
因此,模組注入器的分層結構如下:
#在我們實際的應用中,它很可能是這樣的:
Angular DI 具有分层注入体系,这意味着下级注入器也可以创建它们自己的服务实例。
元素注入器
前面说过,在 Angular 中有两个注入器层次结构,分别是模块注入器和元素注入器。
元素注入器的引入
当 Angular 中懒加载的模块开始广泛使用时,出现了一个 issue:依赖注入系统导致懒加载模块的实例化加倍。
在这一次修复中,引入了新的设计:注入器使用两棵并行的树,一棵用于元素,另一棵用于模块。
Angular 会为所有entryComponents
创建宿主工厂,它们是所有其他组件的根视图。
这意味着每次我们创建动态 Angular 组件时,都会使用根数据(RootData
)创建根视图(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; // 使用根数据创建根视图 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); } // 创建组件 return new ComponentRef_(view, new ViewRef_(view), component); } }
该根数据(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, }; }
引入元素注入器树,原因是这样的设计比较简单。通过更改注入器层次结构,避免交错插入模块和组件注入器,从而导致延迟加载模块的双倍实例化。因为每个注入器都只有一个父对象,并且每次解析都必须精确地寻找一个注入器来检索依赖项。
元素注入器(Element Injector)
在 Angular 中,视图是模板的表示形式,它包含不同类型的节点,其中便有元素节点,元素注入器位于此节点上:
export interface ElementDef { ... // 在该视图中可见的 DI 的公共提供者 publicProviders: {[tokenKey: string]: NodeDef}|null; // 与 visiblePublicProviders 相同,但还包括位于此元素上的私有提供者 allProviders: {[tokenKey: string]: NodeDef}|null; }
默认情况下ElementInjector
为空,除非在@Directive()
或@Component()
的providers
属性中进行配置。
当 Angular 为嵌套的 HTML 元素创建元素注入器时,要么从父元素注入器继承它,要么直接将父元素注入器分配给子节点定义。
如果子 HTML 元素上的元素注入器具有提供者,则应该继承该注入器。否则,无需为子组件创建单独的注入器,并且如果需要,可以直接从父级的注入器中解决依赖项。
元素注入器与模块注入器的设计
那么,元素注入器与模块注入器是从哪个地方开始成为平行树的呢?
我们已经知道,应用程序根模块(AppModule
)会在使用 CLI 的new
命令创建新应用时,自动包含在根AppModule
中。
当应用程序(ApplicationRef
)启动(bootstrap
)时,会创建entryComponent
:
const compRef = componentFactory.create(Injector.NULL, [], selectorOrNode, ngModule);
该过程会使用根数据(RootData
)创建根视图(RootView
),同时会创建根元素注入器,在这里elInjector
为Injector.NULL
。
在这里,Angular 的注入器树被分成元素注入器树和模块注入器树,这两个平行的树了。
Angular 会有规律的创建下级注入器,每当 Angular 创建一个在@Component()
中指定了providers
的组件实例时,它也会为该实例创建一个新的子注入器。类似的,当在运行期间加载一个新的NgModule
时,Angular 也可以为它创建一个拥有自己的提供者的注入器。
子模块和组件注入器彼此独立,并且会为所提供的服务分别创建自己的实例。当 Angular 销毁NgModule
或组件实例时,也会销毁这些注入器以及注入器中的那些服务实例。
Angular 解析依赖过程
上面我们介绍了 Angular 中的两种注入器树:模块注入器树和元素注入器树。那么,Angular 在提供依赖时,又会以怎样的方式去进行解析呢。
在 Angular 种,当为组件/指令解析 token 获取依赖时,Angular 分为两个阶段来解析它:
- 针对
ElementInjector
层次结构(其父级) - 针对
ModuleInjector
层次结构(其父级)
其过程如下(参考多级注入器-解析规则):
当组件声明依赖项时,Angular 会尝试使用它自己的
ElementInjector
来满足该依赖。如果组件的注入器缺少提供者,它将把请求传给其父组件的
ElementInjector
。这些请求将继续转发,直到 Angular 找到可以处理该请求的注入器或用完祖先
ElementInjector
。如果 Angular 在任何
ElementInjector
中都找不到提供者,它将返回到发起请求的元素,并在ModuleInjector
层次结构中进行查找。如果 Angular 仍然找不到提供者,它将引发错误。
为此,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); } }
当 Angular 解析依赖项时,合并注入器则是元素注入器树和模块注入器树之间的桥梁。当 Angular 尝试解析组件或指令中的某些依赖关系时,会使用合并注入器来遍历元素注入器树,然后,如果找不到依赖关系,则切换到模块注入器树以解决依赖关系。
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()
方法中实现:
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
依赖解析过程,解析算法会基于视图层次结构,如图所示进行:
如果在子组件中解析某些令牌,Angular 将:
首先查看子元素注入器,进行检查
elRef.element.allProviders|publicProviders
。然后遍历所有父视图元素(1),并检查元素注入器中的提供者。
如果下一个父视图元素等于
null
(2),则返回到startView
(3),检查startView.rootData.elnjector
(4)。只有在找不到令牌的情况下,才检查
startView.rootData module.injector
( 5 )。
由此可见,Angular 在遍历组件以解析某些依赖性时,将搜索特定视图的父元素而不是特定元素的父元素。视图的父元素可以通过以下方法获得:
// 对于组件视图,这是宿主元素 // 对于嵌入式视图,这是包含视图容器的父节点的索引 export function viewParentEl(view: ViewData): NodeDef|null { const parentView = view.parent; if (parentView) { return view.parentNodeDef !.parent; } else { return null; } }
总结
本文主要介绍了 Angular 中注入器的层级结构,在 Angular 中有两棵平行的注入器树:模块注入器树和元素注入器树。
元素注入器树的引入,主要是为了解决依赖注入解析懒加载模块时,导致模块的双倍实例化问题。在元素注入器树引入后,Angular 解析依赖的过程也有调整,优先寻找元素注入器以及父视图元素注入器等注入器的依赖,只有元素注入器中无法找到令牌时,才会查询模块注入器中的依赖。
更多编程相关知识,请访问:编程入门!!
以上是淺析Angular中的多層次依賴注入設計的詳細內容。更多資訊請關注PHP中文網其他相關文章!

Python和JavaScript的未來趨勢包括:1.Python將鞏固在科學計算和AI領域的地位,2.JavaScript將推動Web技術發展,3.跨平台開發將成為熱門,4.性能優化將是重點。兩者都將繼續在各自領域擴展應用場景,並在性能上有更多突破。

Python和JavaScript在開發環境上的選擇都很重要。 1)Python的開發環境包括PyCharm、JupyterNotebook和Anaconda,適合數據科學和快速原型開發。 2)JavaScript的開發環境包括Node.js、VSCode和Webpack,適用於前端和後端開發。根據項目需求選擇合適的工具可以提高開發效率和項目成功率。

是的,JavaScript的引擎核心是用C語言編寫的。 1)C語言提供了高效性能和底層控制,適合JavaScript引擎的開發。 2)以V8引擎為例,其核心用C 編寫,結合了C的效率和麵向對象特性。 3)JavaScript引擎的工作原理包括解析、編譯和執行,C語言在這些過程中發揮關鍵作用。

JavaScript是現代網站的核心,因為它增強了網頁的交互性和動態性。 1)它允許在不刷新頁面的情況下改變內容,2)通過DOMAPI操作網頁,3)支持複雜的交互效果如動畫和拖放,4)優化性能和最佳實踐提高用戶體驗。

C 和JavaScript通過WebAssembly實現互操作性。 1)C 代碼編譯成WebAssembly模塊,引入到JavaScript環境中,增強計算能力。 2)在遊戲開發中,C 處理物理引擎和圖形渲染,JavaScript負責遊戲邏輯和用戶界面。

JavaScript在網站、移動應用、桌面應用和服務器端編程中均有廣泛應用。 1)在網站開發中,JavaScript與HTML、CSS一起操作DOM,實現動態效果,並支持如jQuery、React等框架。 2)通過ReactNative和Ionic,JavaScript用於開發跨平台移動應用。 3)Electron框架使JavaScript能構建桌面應用。 4)Node.js讓JavaScript在服務器端運行,支持高並發請求。

Python更適合數據科學和自動化,JavaScript更適合前端和全棧開發。 1.Python在數據科學和機器學習中表現出色,使用NumPy、Pandas等庫進行數據處理和建模。 2.Python在自動化和腳本編寫方面簡潔高效。 3.JavaScript在前端開發中不可或缺,用於構建動態網頁和單頁面應用。 4.JavaScript通過Node.js在後端開發中發揮作用,支持全棧開發。

C和C 在JavaScript引擎中扮演了至关重要的角色,主要用于实现解释器和JIT编译器。1)C 用于解析JavaScript源码并生成抽象语法树。2)C 负责生成和执行字节码。3)C 实现JIT编译器,在运行时优化和编译热点代码,显著提高JavaScript的执行效率。


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

Atom編輯器mac版下載
最受歡迎的的開源編輯器

Dreamweaver CS6
視覺化網頁開發工具

EditPlus 中文破解版
體積小,語法高亮,不支援程式碼提示功能

Dreamweaver Mac版
視覺化網頁開發工具

SublimeText3 英文版
推薦:為Win版本,支援程式碼提示!