Angular Renderer (渲染器)的具體使用
Angular 其中的一個設(shè)計目標(biāo)是使瀏覽器與 DOM 獨立。DOM 是復(fù)雜的,因此使組件與它分離,會讓我們的應(yīng)用程序,更容易測試與重構(gòu)。另外的好處是,由于這種解耦,使得我們的應(yīng)用能夠運行在其它平臺 (比如:Node.js、WebWorkers、NativeScript 等)。
為了能夠支持跨平臺,Angular 通過抽象層封裝了不同平臺的差異。比如定義了抽象類 Renderer、Renderer2 、抽象類 RootRenderer 等。此外還定義了以下引用類型:ElementRef、TemplateRef、ViewRef 、ComponentRef 和 ViewContainerRef 等。
本文的主要內(nèi)容是分析 Angular 中 Renderer (渲染器),不過在進行具體分析前,我們先來介紹一下平臺的概念。
平臺
什么是平臺
平臺是應(yīng)用程序運行的環(huán)境。它是一組服務(wù),可以用來訪問你的應(yīng)用程序和 Angular 框架本身的內(nèi)置功能。由于Angular 主要是一個 UI 框架,平臺提供的最重要的功能之一就是頁面渲染。
平臺和引導(dǎo)應(yīng)用程序
在我們開始構(gòu)建一個自定義渲染器之前,我們來看一下如何設(shè)置平臺,以及引導(dǎo)應(yīng)用程序。
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {BrowserModule} from '@angular/platform-browser';
@NgModule({
imports: [BrowserModule],
bootstrap: [AppCmp]
})
class AppModule {}
platformBrowserDynamic().bootstrapModule(AppModule);
如你所見,引導(dǎo)過程由兩部分組成:創(chuàng)建平臺和引導(dǎo)模塊。在這個例子中,我們導(dǎo)入 BrowserModule 模塊,它是瀏覽器平臺的一部分。應(yīng)用中只能有一個激活的平臺,但是我們可以利用它來引導(dǎo)多個模塊,如下所示:
const platformRef: PlatformRef = platformBrowserDynamic(); platformRef.bootstrapModule(AppModule1); platformRef.bootstrapModule(AppModule2);
由于應(yīng)用中只能有一個激活的平臺,單例的服務(wù)必須在該平臺中注冊。比如,瀏覽器只有一個地址欄,對應(yīng)的服務(wù)對象就是單例。此外如何讓我們自定義的 UI 界面,能夠在瀏覽器中顯示出來呢,這就需要使用 Angular 為我們提供的渲染器。
渲染器
什么是渲染器
渲染器是 Angular 為我們提供的一種內(nèi)置服務(wù),用于執(zhí)行 UI 渲染操作。在瀏覽器中,渲染是將模型映射到視圖的過程。模型的值可以是 JavaScript 中的原始數(shù)據(jù)類型、對象、數(shù)組或其它的數(shù)據(jù)對象。然而視圖可以是頁面中的段落、表單、按鈕等其他元素,這些頁面元素內(nèi)部使用 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 相關(guān)的抽象類 (Renderer、Renderer2),我們發(fā)現(xiàn)抽象類中定義了很多抽象方法,用來創(chuàng)建元素、文本、設(shè)置屬性、添加樣式和設(shè)置事件監(jiān)聽等。
渲染器如何工作
在實例化一個組件時,Angular 會調(diào)用 renderComponent() 方法并將其獲取的渲染器與該組件實例相關(guān)聯(lián)。Angular 將會在渲染組件時通過渲染器執(zhí)行對應(yīng)相關(guān)的操作,比如,創(chuàng)建元素、設(shè)置屬性、添加樣式和訂閱事件等。

使用 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');
}
}
以上代碼中,我們利用構(gòu)造注入的方式,注入 Renderer2 和 ElementRef 實例。有些讀者可能會問,注入的實例對象是怎么生成的。這里我們只是稍微介紹一下相關(guān)知識,并不會詳細(xì)展開。具體代碼如下:
// 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 等
}
}
}
// ...
}
通過以上代碼,我們發(fā)現(xiàn)當(dāng)我們在組件類的構(gòu)造函數(shù)中聲明相應(yīng)的依賴對象時,如 Renderer2 和 ElementRef,Angular 內(nèi)部會調(diào)用 resolveDep() 方法,實例化 Token 對應(yīng)依賴對象。
在大多數(shù)情況下,我們開發(fā)的 Angular 應(yīng)用程序是運行在瀏覽器平臺,接下來我們來了解一下該平臺下的默認(rèn)渲染器 - DefaultDomRenderer2。
DefaultDomRenderer2
在瀏覽器平臺下,我們可以通過調(diào)用 DomRendererFactory2 工廠,根據(jù)不同的視圖封裝方案,創(chuàng)建對應(yīng)渲染器。
// 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)建默認(rèn)的DOM渲染器
this.defaultRenderer = new DefaultDomRenderer2(eventManager);
};
createRenderer(element: any, type: RendererType2|null): Renderer2 {
if (!element || !type) {
return this.defaultRenderer;
}
// 根據(jù)不同的視圖封裝方案,創(chuàng)建不同的渲染器
switch (type.encapsulation) {
// 無 Shadow DOM,但是通過 Angular 提供的樣式包裝機制來封裝組件,
// 使得組件的樣式不受外部影響,這是 Angular 的默認(rèn)設(shè)置。
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 類的內(nèi)部實現(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 內(nèi)部如何利用它們。
DomRendererFactory2 內(nèi)部應(yīng)用
// 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) {
// 用于判斷應(yīng)用中是否已經(jīng)導(dǎo)入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);
}
步驟一
當(dāng) Angular 在創(chuàng)建組件視圖時,會根據(jù) 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;
// 設(shè)置組件渲染器的類型
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 屬性值對應(yīng)的 renderer 對象。若該值不為空,則調(diào)用 parentView.root 對象的 rendererFactory() 方法創(chuàng)建 renderer 對象。
通過上面分析,我們發(fā)現(xiàn)不管走哪條分支,我們都需要使用 parentView.root 對象,然而該對象是什么特殊對象?我們發(fā)現(xiàn) parentView 的數(shù)據(jù)類型是 ViewData ,該數(shù)據(jù)接口定義如下:
// 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)在問題來了:
- 什么時候創(chuàng)建
RootData對象? - 怎么創(chuàng)建
RootData對象?
什么時候創(chuàng)建 RootData 對象?
當(dāng)創(chuàng)建根視圖的時候會創(chuàng)建 RootData,在開發(fā)環(huán)境會調(diào)用 debugCreateRootView() 方法創(chuàng)建 RootView,而在生產(chǎn)環(huán)境會調(diào)用 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;
}
上面代碼中,當(dāng)創(chuàng)建 RootView 的時候,會調(diào)用 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 渲染器的相關(guān)基礎(chǔ)知識已介紹完畢。接下來,我們做一個簡單總結(jié):
- Angular 應(yīng)用程序啟動時會創(chuàng)建 RootView (生產(chǎn)環(huán)境下通過調(diào)用 createProdRootView() 方法)
- 創(chuàng)建 RootView 的過程中,會創(chuàng)建 RootData 對象,該對象可以通過 ViewData 的 root 屬性訪問到?;?RootData 對象,我們可以通過
renderer訪問到默認(rèn)的渲染器,即 DefaultDomRenderer2 實例,此外也可以通過rendererFactory訪問到RendererFactory2實例。 - 在創(chuàng)建組件視圖 (ViewData) 時,會根據(jù)
componentRendererType的屬性值,來設(shè)置組件關(guān)聯(lián)的renderer渲染器。 - 當(dāng)渲染組件視圖的時候,Angular 會利用該組件關(guān)聯(lián)的
renderer提供的 API,創(chuàng)建該視圖中的節(jié)點或執(zhí)行視圖的相關(guān)操作,比如創(chuàng)建元素 (createElement)、創(chuàng)建文本 (createText)、設(shè)置樣式 (setStyle) 和 設(shè)置事件監(jiān)聽 (listen) 等。
后面如果有時間的話,我們會介紹如何自定義渲染器,有興趣的讀者,可以先查閱 "參考資源" 中的鏈接。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 老生常談angularjs中的$state.go
- 詳解在Angularjs中ui-sref和$state.go如何傳遞參數(shù)
- Angular中$state.go頁面跳轉(zhuǎn)并傳遞參數(shù)的方法
- 監(jiān)聽angularJs列表數(shù)據(jù)是否渲染完畢的方法示例
- 使用Angular CDK實現(xiàn)一個Service彈出Toast組件功能
- Angular?服務(wù)器端渲染應(yīng)用常見的內(nèi)存泄漏問題小結(jié)
- JS?Angular?服務(wù)器端渲染應(yīng)用設(shè)置渲染超時時間???????
- Angular?服務(wù)器端渲染緩存功能問題
- 使用?Angular?服務(wù)器端渲染?Transfer?State?Service
相關(guān)文章
AngularJS基礎(chǔ) ng-hide 指令用法及示例代碼
本文主要介紹AngularJS ng-hide 指令,這里整理了ng-hide指令的基礎(chǔ)資料,并附實例代碼,有興趣的小伙伴參考下2016-08-08
AngularJS ng-bind-template 指令詳解
本文注意介紹AngularJS ng-bind-template 指令,這里把相關(guān)資料整理了一下,并附實例代碼,有需要的小伙伴可以參考下2016-07-07
AngularJS標(biāo)簽頁tab選項卡切換功能經(jīng)典實例詳解
這篇文章主要介紹了AngularJS實現(xiàn)標(biāo)簽頁tab選項卡功能,結(jié)合實例形式總結(jié)分析了各種常用的tab選項卡切換操作實現(xiàn)技巧與相關(guān)操作注意事項,需要的朋友可以參考下2018-05-05
詳解angular分頁插件tm.pagination二次觸發(fā)問題解決方案
這篇文章主要介紹了詳解angular分頁插件tm.pagination二次觸發(fā)問題解決方案,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-07-07

