Angular 其中的一個設計目標是使瀏覽器與 DOM 獨立。DOM 是復雜的,因此使組件與它分離,會讓我們的應用程序,更容易測試與重構。另外的好處是,由于這種解耦,使得我們的應用能夠運行在其它平臺 (比如:Node.js、WebWorkers、NativeScript 等)。
為了能夠支持跨平臺,Angular 通過抽象層封裝了不同平臺的差異。比如定義了抽象類 Renderer、Renderer2 、抽象類 RootRenderer 等。此外還定義了以下引用類型:ElementRef、TemplateRef、ViewRef 、ComponentRef 和 ViewContainerRef 等。
本文的主要內容是分析 Angular 中 Renderer (渲染器),不過在進行具體分析前,我們先來介紹一下平臺的概念。
平臺
什么是平臺
平臺是應用程序運行的環(huán)境。它是一組服務,可以用來訪問你的應用程序和 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);
如你所見,引導過程由兩部分組成:創(chuàng)建平臺和引導模塊。在這個例子中,我們導入 BrowserModule 模塊,它是瀏覽器平臺的一部分。應用中只能有一個激活的平臺,但是我們可以利用它來引導多個模塊,如下所示:
const platformRef: PlatformRef = platformBrowserDynamic(); platformRef.bootstrapModule(AppModule1); platformRef.bootstrapModule(AppModule2);
由于應用中只能有一個激活的平臺,單例的服務必須在該平臺中注冊。比如,瀏覽器只有一個地址欄,對應的服務對象就是單例。此外如何讓我們自定義的 UI 界面,能夠在瀏覽器中顯示出來呢,這就需要使用 Angular 為我們提供的渲染器。
渲染器
什么是渲染器
渲染器是 Angular 為我們提供的一種內置服務,用于執(zhí)行 UI 渲染操作。在瀏覽器中,渲染是將模型映射到視圖的過程。模型的值可以是 JavaScript 中的原始數(shù)據類型、對象、數(shù)組或其它的數(shù)據對象。然而視圖可以是頁面中的段落、表單、按鈕等其他元素,這些頁面元素內部使用 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),我們發(fā)現(xiàn)抽象類中定義了很多抽象方法,用來創(chuàng)建元素、文本、設置屬性、添加樣式和設置事件監(jiān)聽等。
渲染器如何工作
在實例化一個組件時,Angular 會調用 renderComponent()
方法并將其獲取的渲染器與該組件實例相關聯(lián)。Angular 將會在渲染組件時通過渲染器執(zhí)行對應相關的操作,比如,創(chuàng)建元素、設置屬性、添加樣式和訂閱事件等。
使用 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.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);
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 等 } } } // ... }
通過以上代碼,我們發(fā)現(xiàn)當我們在組件類的構造函數(shù)中聲明相應的依賴對象時,如 Renderer2 和 ElementRef,Angular 內部會調用 resolveDep()
方法,實例化 Token 對應依賴對象。
在大多數(shù)情況下,我們開發(fā)的 Angular 應用程序是運行在瀏覽器平臺,接下來我們來了解一下該平臺下的默認渲染器 - DefaultDomRenderer2。
DefaultDomRenderer2
在瀏覽器平臺下,我們可以通過調用 DomRendererFactory2
工廠,根據不同的視圖封裝方案,創(chuàng)建對應渲染器。
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) { // 創(chuàng)建默認的DOM渲染器 this.defaultRenderer = new DefaultDomRenderer2(eventManager); }; createRenderer(element: any, type: RendererType2|null): Renderer2 { if (!element || !type) { return this.defaultRenderer; } // 根據不同的視圖封裝方案,創(chuàng)建不同的渲染器 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 類的內部實現(xiàn):
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 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.`); } } }
createComponentView()
// 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 在創(chuàng)建組件視圖時,會根據 nodeDef.element
對象的 componentRendererType
屬性值,來創(chuàng)建組件的渲染器。接下來我們先來看一下 NodeDef
、 ElementDef
和 RendererType2
接口定義:
// packages/core/src/view/types.ts // 視圖中節(jié)點的定義 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()
方法創(chuàng)建 renderer
對象。
通過上面分析,我們發(fā)現(xiàn)不管走哪條分支,我們都需要使用 parentView.root
對象,然而該對象是什么特殊對象?我們發(fā)現(xiàn) parentView
的數(shù)據類型是 ViewData
,該數(shù)據接口定義如下:
// 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
的接口定義,我們終于發(fā)現(xiàn)了 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; }
那好,現(xiàn)在問題來了:
RootData
對象?RootData
對象?什么時候創(chuàng)建 RootData
對象?
當創(chuàng)建根視圖的時候會創(chuàng)建 RootData,在開發(fā)環(huán)境會調用 debugCreateRootView()
方法創(chuàng)建 RootView
,而在生產環(huán)境會調用 createProdRootView()
方法創(chuàng)建 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); } // 創(chuàng)建根視圖 export function createRootView(root: RootData, def: ViewDefinition, context?: any): ViewData { // 創(chuàng)建ViewData對象 const view = createView(root, root.renderer, null, null, def); initView(view, context, context); createViewNodes(view); return view; }
上面代碼中,當創(chuàng)建 RootView
的時候,會調用 createRootData()
方法創(chuàng)建 RootData
對象。最后一步就是分析 createRootData()
方法。
怎么創(chuàng)建 RootData
對象?
通過上面分析,我們知道通過 createRootData()
方法,來創(chuàng)建 RootData
對象。createRootData()
方法具體實現(xiàn)如下:
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); // 創(chuàng)建RootRenderer const renderer = rendererFactory.createRenderer(null, null); return { ngModule, injector: elInjector, projectableNodes, selectorOrNode: rootSelectorOrNode, sanitizer, rendererFactory, renderer, errorHandler }; }
此時瀏覽器平臺下, Renderer
渲染器的相關基礎知識已介紹完畢。接下來,我們做一個簡單總結:
renderer
訪問到默認的渲染器,即 DefaultDomRenderer2 實例,此外也可以通過 rendererFactory
訪問到 RendererFactory2
實例。componentRendererType
的屬性值,來設置組件關聯(lián)的 renderer
渲染器。renderer
提供的 API,創(chuàng)建該視圖中的節(jié)點或執(zhí)行視圖的相關操作,比如創(chuàng)建元素 (createElement)、創(chuàng)建文本 (createText)、設置樣式 (setStyle) 和 設置事件監(jiān)聽 (listen) 等。后面如果有時間的話,我們會介紹如何自定義渲染器,有興趣的讀者,可以先查閱 "參考資源" 中的鏈接。
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯(lián)系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com