阅读 Angular 6/RxJS 最新教程,请访问 前端修仙之路
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
RootRenderer
export abstract class RootRenderer {abstract renderComponent(componentType: RenderComponentType): Renderer;}
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;// ...}
Renderer2
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 实例。有些读者可能会问,注入的实例对象是怎么生成的。这里我们只是稍微介绍一下相关知识,并不会详细展开。具体代码如下:
TokenKey
// packages/core/src/view/util.tsconst _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.tsconst 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);
resolveDep()
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
工厂,根据不同的视图封装方案,创建对应渲染器。
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 内部应用
BrowserModule
// packages/platform-browser/src/browser.ts@NgModule({providers: [// 配置 DomRendererFactory2 和 RendererFactory2 providerDomRendererFactory2,{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.`);}}}
createComponentView()
// packages/core/src/view/view.tsexport 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.elementprovider: ProviderDef|null;// ...}// 元素的定义export interface ElementDef {name: string|null;attrs: [string, string, string][]|null;template: ViewDefinition|null;componentProvider: NodeDef|null;// 设置组件渲染器的类型componentRendererType: RendererType2|null; // ponentRendererTypecomponentView: ViewDefinitionFactory|null;handleEvent: ElementHandleEventFn|null;// ...}// packages/core/src/render/api.ts// RendererType2 接口定义export interface RendererType2 {id: string;encapsulation: ViewEncapsulation; // Emulated、Native、Nonestyles: (string|any[])[];data: {[kind: string]: any};}
步骤二
获取componentRendererType
的属性值后,如果该值为null
的话,则直接使用parentView.root
属性值对应的renderer
对象。若该值不为空,则调用parentView.root
对象的rendererFactory()
方法创建renderer
对象。
通过上面分析,我们发现不管走哪条分支,我们都需要使用parentView.root
对象,然而该对象是什么特殊对象?我们发现parentView
的数据类型是ViewData
,该数据接口定义如下:
// packages/core/src/view/types.tsexport 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.tsexport 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);// 创建RootRendererconst 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) 等。
后面如果有时间的话,我们会介绍如何自定义渲染器,有兴趣的读者,可以先查阅 "参考资源" 中的链接。