Angular Renderer (渲染器)的具体使用


Posted in Javascript onMay 03, 2018

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 将会在渲染组件时通过渲染器执行对应相关的操作,比如,创建元素、设置属性、添加样式和订阅事件等。

Angular Renderer (渲染器)的具体使用

使用 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 等
  }
 }
 }
 // ...
}

通过以上代码,我们发现当我们在组件类的构造函数中声明相应的依赖对象时,如 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;
  }
 }
 }
}

上面代码中的 EmulatedEncapsulationDomRenderer2ShadowDomRenderer 类都继承于 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;
 }
}

介绍完 DomRendererFactory2DefaultDomRenderer2 类,最后我们来看一下 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 在创建组件视图时,会根据 nodeDef.element 对象的 componentRendererType 属性值,来创建组件的渲染器。接下来我们先来看一下 NodeDefElementDefRendererType2 接口定义:

// 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;
}

那好,现在问题来了:

  1. 什么时候创建 RootData 对象?
  2. 怎么创建 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 渲染器的相关基础知识已介绍完毕。接下来,我们做一个简单总结:

  1. Angular 应用程序启动时会创建 RootView (生产环境下通过调用 createProdRootView() 方法)
  2. 创建 RootView 的过程中,会创建 RootData 对象,该对象可以通过 ViewData 的 root 属性访问到。基于 RootData 对象,我们可以通过 renderer 访问到默认的渲染器,即 DefaultDomRenderer2 实例,此外也可以通过 rendererFactory 访问到 RendererFactory2 实例。
  3. 在创建组件视图 (ViewData) 时,会根据 componentRendererType 的属性值,来设置组件关联的 renderer 渲染器。
  4. 当渲染组件视图的时候,Angular 会利用该组件关联的 renderer 提供的 API,创建该视图中的节点或执行视图的相关操作,比如创建元素 (createElement)、创建文本 (createText)、设置样式 (setStyle) 和 设置事件监听 (listen) 等。

后面如果有时间的话,我们会介绍如何自定义渲染器,有兴趣的读者,可以先查阅 "参考资源" 中的链接。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Jquery的Tabs内容轮换效果实现代码,几行搞定
Feb 12 Javascript
JS根据变量保存方法名并执行方法示例
Apr 04 Javascript
jQuery 处理页面的事件详解
Jan 20 Javascript
javascript实现table选中的行以指定颜色高亮显示的方法
May 13 Javascript
javascript作用域问题实例分析
Jul 13 Javascript
Grunt入门教程(自动任务运行器)
Aug 06 Javascript
jQuery实现仿腾讯视频列表分页效果的方法
Aug 07 Javascript
JS中使用DOM来控制HTML元素
Jul 31 Javascript
Angular ElementRef简介及其使用
Oct 01 Javascript
详解ES6 Fetch API HTTP请求实用指南
Nov 14 Javascript
jquery图片预览插件实现方法详解
Jul 18 jQuery
el-form 多层级表单的实现示例
Sep 10 Javascript
react router4+redux实现路由权限控制的方法
May 03 #Javascript
vue.js学习笔记之v-bind和v-on解析
May 03 #Javascript
jQuery实现每隔一段时间自动更换样式的方法分析
May 03 #jQuery
详解VueJs中的V-bind指令
May 03 #Javascript
基于vue,vue-router, vuex及addRoutes进行权限控制问题
May 02 #Javascript
用ES6写全屏滚动插件的示例代码
May 02 #Javascript
详解Vue中watch的高级用法
May 02 #Javascript
You might like
分页显示Oracle数据库记录的类之一
2006/10/09 PHP
shopex中集成的站长统计功能的代码简单分析
2011/08/11 PHP
CodeIgniter中使用cookie的三种方式详解
2014/07/18 PHP
JavaScript flash复制库类 Zero Clipboard
2011/01/17 Javascript
JS获取页面input控件中所有text控件并追加样式属性
2013/02/25 Javascript
Flexigrid在IE下不显示数据的有效处理方法
2014/09/04 Javascript
javascript 数组去重复(在线去重工具)
2016/12/17 Javascript
JS操作input标签属性checkbox全选的实现代码
2017/03/02 Javascript
js实现时间轴自动排列效果
2017/03/09 Javascript
Angular使用Md5加密的解决方法
2017/09/16 Javascript
angular中两种表单的区别(响应式和模板驱动表单)
2018/12/06 Javascript
微信小程序实现判断是分享到群还是个人功能示例
2019/05/03 Javascript
使用Node.js写一个代码生成器的方法步骤
2019/05/10 Javascript
nodemon实现Typescript项目热更新的示例代码
2019/11/19 Javascript
[04:41]2014DOTA2国际邀请赛 Liquid顺利突围晋级正赛
2014/07/09 DOTA
python操作MongoDB基础知识
2013/11/01 Python
常见的在Python中实现单例模式的三种方法
2015/04/08 Python
Python简单的制作图片验证码实例
2017/05/31 Python
基于python 处理中文路径的终极解决方法
2018/04/12 Python
Python 判断图像是否读取成功的方法
2019/01/26 Python
解决Pycharm后台indexing导致不能run的问题
2019/06/27 Python
简单了解Python matplotlib线的属性
2019/06/29 Python
Python中bisect的使用方法
2019/12/31 Python
np.dot()函数的用法详解
2020/01/17 Python
tensorflow 实现自定义梯度反向传播代码
2020/02/10 Python
微信小程序“圣诞帽”的实现思路详解
2017/12/28 HTML / CSS
美国羊皮公司:Overland
2018/01/15 全球购物
英国地毯卖家:The Rug Seller
2019/07/18 全球购物
物流专业大学应届生求职信
2013/11/03 职场文书
大专生的学习自我评价
2013/12/04 职场文书
劳动实践课感言
2014/02/01 职场文书
会计与出纳自荐书范文
2014/03/16 职场文书
幼儿园课题方案
2014/06/09 职场文书
谢师宴邀请函
2015/02/02 职场文书
Java8 Stream API 提供了一种高效且易于使用的处理数据的方式
2022/04/13 Java/Android
python解析json数据
2022/04/29 Python