Angular Renderer (渲染器)的具体使用
angular 其中的一个设计目标是使浏览器与 dom 独立。dom 是复杂的,因此使组件与它分离,会让我们的应用程序,更容易测试与重构。另外的好处是,由于这种解耦,使得我们的应用能够运行在其它平台 (比如:node.js、webworkers、nativescript 等)。
为了能够支持跨平台,angular 通过抽象层封装了不同平台的差异。比如定义了抽象类 renderer、renderer2 、抽象类 rootrenderer 等。此外还定义了以下引用类型:elementref、templateref、viewref 、componentref 和 viewcontainerref 等。
本文的主要内容是分析 angular 中 renderer (渲染器),不过在进行具体分析前,我们先来介绍一下平台的概念。
平台
什么是平台
平台是应用程序运行的环境。它是一组服务,可以用来访问你的应用程序和 angular 框架本身的内置功能。由于angular 主要是一个 ui 框架,平台提供的最重要的功能之一就是页面渲染。
平台和引导应用程序
在我们开始构建一个自定义渲染器之前,我们来看一下如何设置平台,以及引导应用程序。
import {platformbrowserdynamic} from '@angular/platform-browser-dynamic'; import {browsermodule} from '@angular/platform-browser'; @ngmodule({ imports: [browsermodule], bootstrap: [appcmp] }) class appmodule {} platformbrowserdynamic().bootstrapmodule(appmodule);
如你所见,引导过程由两部分组成:创建平台和引导模块。在这个例子中,我们导入 browsermodule 模块,它是浏览器平台的一部分。应用中只能有一个激活的平台,但是我们可以利用它来引导多个模块,如下所示:
const platformref: platformref = platformbrowserdynamic(); platformref.bootstrapmodule(appmodule1); platformref.bootstrapmodule(appmodule2);
由于应用中只能有一个激活的平台,单例的服务必须在该平台中注册。比如,浏览器只有一个地址栏,对应的服务对象就是单例。此外如何让我们自定义的 ui 界面,能够在浏览器中显示出来呢,这就需要使用 angular 为我们提供的渲染器。
渲染器
什么是渲染器
渲染器是 angular 为我们提供的一种内置服务,用于执行 ui 渲染操作。在浏览器中,渲染是将模型映射到视图的过程。模型的值可以是 javascript 中的原始数据类型、对象、数组或其它的数据对象。然而视图可以是页面中的段落、表单、按钮等其他元素,这些页面元素内部使用 dom (document object model) 来表示。
angular renderer
export abstract class rootrenderer { abstract rendercomponent(componenttype: rendercomponenttype): renderer; }
/** * @deprecated use the `renderer2` instead. */ export abstract class renderer { abstract createelement(parentelement: any, name: string, debuginfo?: renderdebuginfo): any; abstract createtext(parentelement: any, value: string, debuginfo?: renderdebuginfo): any; abstract listen(renderelement: any, name: string, callback: function): function; abstract listenglobal(target: string, name: string, callback: function): function; abstract setelementproperty(renderelement: any, propertyname: string, propertyvalue: any): void; abstract setelementattribute(renderelement: any, attributename: string, attributevalue: string): void; // ... }
export abstract class renderer2 { abstract createelement(name: string, namespace?: string|null): any; abstract createcomment(value: string): any; abstract createtext(value: string): any; abstract setattribute(el: any, name: string, value: string, namespace?: string|null): void; abstract removeattribute(el: any, name: string, namespace?: string|null): void; abstract addclass(el: any, name: string): void; abstract removeclass(el: any, name: string): void; abstract setstyle(el: any, style: string, value: any, flags?: rendererstyleflags2): void; abstract removestyle(el: any, style: string, flags?: rendererstyleflags2): void; abstract setproperty(el: any, name: string, value: any): void; abstract setvalue(node: any, value: string): void; abstract listen( target: 'window'|'document'|'body'|any, eventname: string, callback: (event: any) => boolean | void): () => void; }
需要注意的是在 angular 4.x+ 版本,我们使用 renderer2
替代 renderer
。通过观察 renderer 相关的抽象类 (renderer、renderer2),我们发现抽象类中定义了很多抽象方法,用来创建元素、文本、设置属性、添加样式和设置事件监听等。
渲染器如何工作
在实例化一个组件时,angular 会调用 rendercomponent()
方法并将其获取的渲染器与该组件实例相关联。angular 将会在渲染组件时通过渲染器执行对应相关的操作,比如,创建元素、设置属性、添加样式和订阅事件等。
使用 renderer
@component({ selector: 'exe-cmp', template: ` <h3>exe component</h3> ` }) export class execomponent { constructor(private renderer: renderer2, elref: elementref) { this.renderer.setproperty(elref.nativeelement, 'author', 'semlinker'); } }
以上代码中,我们利用构造注入的方式,注入 renderer2 和 elementref 实例。有些读者可能会问,注入的实例对象是怎么生成的。这里我们只是稍微介绍一下相关知识,并不会详细展开。具体代码如下:
// packages/core/src/view/util.ts const _tokenkeycache = new map<any, string>(); export function tokenkey(token: any): string { let key = _tokenkeycache.get(token); if (!key) { key = stringify(token) + '_' + _tokenkeycache.size; _tokenkeycache.set(token, key); } return key; } // packages/core/src/view/provider.ts const rendererv1tokenkey = tokenkey(rendererv1); const renderer2tokenkey = tokenkey(renderer2); const elementreftokenkey = tokenkey(elementref); const viewcontainerreftokenkey = tokenkey(viewcontainerref); const templatereftokenkey = tokenkey(templateref); const changedetectorreftokenkey = tokenkey(changedetectorref); const injectorreftokenkey = tokenkey(injector);
export function resolvedep( view: viewdata, eldef: nodedef, allowprivateservices: boolean, depdef: depdef, notfoundvalue: any = injector.throw_if_not_found): any { const tokenkey = depdef.tokenkey; // ... while (view) { if (eldef) { switch (tokenkey) { case rendererv1tokenkey: { // tokenkey(rendererv1) const compview = findcompview(view, eldef, allowprivateservices); return createrendererv1(compview); } case renderer2tokenkey: { // tokenkey(renderer2) const compview = findcompview(view, eldef, allowprivateservices); return compview.renderer; } case elementreftokenkey: // tokenkey(elementref) return new elementref(aselementdata(view, eldef.index).renderelement); // ... 此外还包括:viewcontainerreftokenkey、templatereftokenkey、 // changedetectorreftokenkey 等 } } } // ... }
通过以上代码,我们发现当我们在组件类的构造函数中声明相应的依赖对象时,如 renderer2 和 elementref,angular 内部会调用 resolvedep()
方法,实例化 token 对应依赖对象。
在大多数情况下,我们开发的 angular 应用程序是运行在浏览器平台,接下来我们来了解一下该平台下的默认渲染器 - defaultdomrenderer2。
defaultdomrenderer2
在浏览器平台下,我们可以通过调用 domrendererfactory2
工厂,根据不同的视图封装方案,创建对应渲染器。
// packages/platform-browser/src/dom/dom_renderer.ts @injectable() export class domrendererfactory2 implements rendererfactory2 { private rendererbycompid = new map<string, renderer2>(); private defaultrenderer: renderer2; constructor( private eventmanager: eventmanager, private sharedstyleshost: domsharedstyleshost) { // 创建默认的dom渲染器 this.defaultrenderer = new defaultdomrenderer2(eventmanager); }; createrenderer(element: any, type: renderertype2|null): renderer2 { if (!element || !type) { return this.defaultrenderer; } // 根据不同的视图封装方案,创建不同的渲染器 switch (type.encapsulation) { // 无 shadow dom,但是通过 angular 提供的样式包装机制来封装组件, // 使得组件的样式不受外部影响,这是 angular 的默认设置。 case viewencapsulation.emulated: { let renderer = this.rendererbycompid.get(type.id); if (!renderer) { renderer = new emulatedencapsulationdomrenderer2(this.eventmanager, this.sharedstyleshost, type); this.rendererbycompid.set(type.id, renderer); } (<emulatedencapsulationdomrenderer2>renderer).applytohost(element); return renderer; } // 使用原生的 shadow dom 特性 case viewencapsulation.native: return new shadowdomrenderer(this.eventmanager, this.sharedstyleshost, element, type); // 无 shadow dom,并且也无样式包装 default: { // ... return this.defaultrenderer; } } } }
上面代码中的 emulatedencapsulationdomrenderer2
和 shadowdomrenderer
类都继承于 defaultdomrenderer2
类,接下来我们再来看一下 defaultdomrenderer2 类的内部实现:
class defaultdomrenderer2 implements renderer2 { constructor(private eventmanager: eventmanager) {} // 省略 renderer2 抽象类中定义的其它方法 createelement(name: string, namespace?: string): any { if (namespace) { return document.createelementns(namespace_uris[namespace], name); } return document.createelement(name); } createcomment(value: string): any { return document.createcomment(value); } createtext(value: string): any { return document.createtextnode(value); } addclass(el: any, name: string): void { el.classlist.add(name); } setstyle(el: any, style: string, value: any, flags: rendererstyleflags2): void { if (flags & rendererstyleflags2.dashcase) { el.style.setproperty( style, value, !!(flags & rendererstyleflags2.important) ? 'important' : ''); } else { el.style[style] = value; } } listen( target: 'window'|'document'|'body'|any, event: string, callback: (event: any) => boolean): () => void { checknosyntheticprop(event, 'listener'); if (typeof target === 'string') { return <() => void>this.eventmanager.addglobaleventlistener( target, event, decoratepreventdefault(callback)); } return <() => void>this.eventmanager.addeventlistener( target, event, decoratepreventdefault(callback)) as() => void; } }
介绍完 domrendererfactory2
和 defaultdomrenderer2
类,最后我们来看一下 angular 内部如何利用它们。
domrendererfactory2 内部应用
// packages/platform-browser/src/browser.ts @ngmodule({ providers: [ // 配置 domrendererfactory2 和 rendererfactory2 provider domrendererfactory2, {provide: rendererfactory2, useexisting: domrendererfactory2}, // ... ], exports: [commonmodule, applicationmodule] }) export class browsermodule { constructor(@optional() @skipself() parentmodule: browsermodule) { // 用于判断应用中是否已经导入browsermodule模块 if (parentmodule) { throw new error( `browsermodule has already been loaded. if you need access to common directives such as ngif and ngfor from a lazy loaded module, import commonmodule instead.`); } } }
// packages/core/src/view/view.ts export function createcomponentview( parentview: viewdata, nodedef: nodedef, viewdef: viewdefinition, hostelement: any): viewdata { const renderertype = nodedef.element !.componentrenderertype; // 步骤一 let comprenderer: renderer2; if (!renderertype) { // 步骤二 comprenderer = parentview.root.renderer; } else { comprenderer = parentview.root.rendererfactory .createrenderer(hostelement, renderertype); } return createview( parentview.root, comprenderer, parentview, nodedef.element !.componentprovider, viewdef); }
步骤一
当 angular 在创建组件视图时,会根据 nodedef.element
对象的 componentrenderertype
属性值,来创建组件的渲染器。接下来我们先来看一下 nodedef
、 elementdef
和 renderertype2
接口定义:
// packages/core/src/view/types.ts // 视图中节点的定义 export interface nodedef { bindingindex: number; bindings: bindingdef[]; bindingflags: bindingflags; outputs: outputdef[]; element: elementdef|null; // nodedef.element provider: providerdef|null; // ... } // 元素的定义 export interface elementdef { name: string|null; attrs: [string, string, string][]|null; template: viewdefinition|null; componentprovider: nodedef|null; // 设置组件渲染器的类型 componentrenderertype: renderertype2|null; // nodedef.element.componentrenderertype componentview: viewdefinitionfactory|null; handleevent: elementhandleeventfn|null; // ... } // packages/core/src/render/api.ts // renderertype2 接口定义 export interface renderertype2 { id: string; encapsulation: viewencapsulation; // emulated、native、none styles: (string|any[])[]; data: {[kind: string]: any}; }
步骤二
获取 componentrenderertype
的属性值后,如果该值为 null
的话,则直接使用 parentview.root
属性值对应的 renderer
对象。若该值不为空,则调用 parentview.root
对象的 rendererfactory()
方法创建 renderer
对象。
通过上面分析,我们发现不管走哪条分支,我们都需要使用 parentview.root
对象,然而该对象是什么特殊对象?我们发现 parentview
的数据类型是 viewdata
,该数据接口定义如下:
// packages/core/src/view/types.ts export interface viewdata { def: viewdefinition; root: rootdata; renderer: renderer2; nodes: {[key: number]: nodedata}; state: viewstate; oldvalues: any[]; disposables: disposablefn[]|null; // ... }
通过 viewdata
的接口定义,我们终于发现了 parentview.root
的属性类型,即 rootdata
:
// packages/core/src/view/types.ts export interface rootdata { injector: injector; ngmodule: ngmoduleref<any>; projectablenodes: any[][]; selectorornode: any; renderer: renderer2; rendererfactory: rendererfactory2; errorhandler: errorhandler; sanitizer: sanitizer; }
那好,现在问题来了:
- 什么时候创建
rootdata
对象? - 怎么创建
rootdata
对象?
什么时候创建 rootdata
对象?
当创建根视图的时候会创建 rootdata,在开发环境会调用 debugcreaterootview()
方法创建 rootview
,而在生产环境会调用 createprodrootview()
方法创建 rootview
。简单起见,我们只分析 createprodrootview()
方法:
function createprodrootview( elinjector: injector, projectablenodes: any[][], rootselectorornode: string | any, def: viewdefinition, ngmodule: ngmoduleref<any>, context?: any): viewdata { /** rendererfactory2 provider 配置 * domrendererfactory2, * {provide: rendererfactory2, useexisting: domrendererfactory2}, */ const rendererfactory: rendererfactory2 = ngmodule.injector.get(rendererfactory2); return createrootview( createrootdata(elinjector, ngmodule, rendererfactory, projectablenodes, rootselectorornode), def, context); } // 创建根视图 export function createrootview(root: rootdata, def: viewdefinition, context?: any): viewdata { // 创建viewdata对象 const view = createview(root, root.renderer, null, null, def); initview(view, context, context); createviewnodes(view); return view; }
上面代码中,当创建 rootview
的时候,会调用 createrootdata()
方法创建 rootdata
对象。最后一步就是分析 createrootdata()
方法。
怎么创建 rootdata
对象?
通过上面分析,我们知道通过 createrootdata()
方法,来创建 rootdata
对象。createrootdata()
方法具体实现如下:
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); // 创建rootrenderer const renderer = rendererfactory.createrenderer(null, null); return { ngmodule, injector: elinjector, projectablenodes, selectorornode: rootselectorornode, sanitizer, rendererfactory, renderer, errorhandler }; }
此时浏览器平台下, renderer
渲染器的相关基础知识已介绍完毕。接下来,我们做一个简单总结:
- angular 应用程序启动时会创建 rootview (生产环境下通过调用 createprodrootview() 方法)
- 创建 rootview 的过程中,会创建 rootdata 对象,该对象可以通过 viewdata 的 root 属性访问到。基于 rootdata 对象,我们可以通过
renderer
访问到默认的渲染器,即 defaultdomrenderer2 实例,此外也可以通过rendererfactory
访问到rendererfactory2
实例。 - 在创建组件视图 (viewdata) 时,会根据
componentrenderertype
的属性值,来设置组件关联的renderer
渲染器。 - 当渲染组件视图的时候,angular 会利用该组件关联的
renderer
提供的 api,创建该视图中的节点或执行视图的相关操作,比如创建元素 (createelement)、创建文本 (createtext)、设置样式 (setstyle) 和 设置事件监听 (listen) 等。
后面如果有时间的话,我们会介绍如何自定义渲染器,有兴趣的读者,可以先查阅 "参考资源" 中的链接。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: Angular父组件调用子组件的方法