您好,欢迎访问一九零五行业门户网

浅析Angular中的多级依赖注入设计

本篇文章带大家进行angular源码学习,介绍一下多级依赖注入设计,希望对大家有所帮助!
作为“为大型前端项目”而设计的前端框架,angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文主要围绕 angular 中的最大特点——依赖注入,介绍 angular 中多级依赖注入的设计。【相关教程推荐:《angular教程》】
上一篇我们介绍了 angular 中的injectot注入器、provider提供者,以及注入器机制。那么,在 angular 应用中,各个组件和模块间又是怎样共享依赖的,同样的服务是否可以多次实例化呢?
组件和模块的依赖注入过程,离不开 angular 多级依赖注入的设计,我们来看看。
多级依赖注入前面我们说过,angular 中的注入器是可继承、且分层的。
在 angular 中,有两个注入器层次结构:
moduleinjector模块注入器:使用@ngmodule()或@injectable()注解在此层次结构中配置moduleinjectorelementinjector元素注入器:在每个 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中的多级依赖注入设计的详细内容。
其它类似信息

推荐信息