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 相关文章推荐
range 标准化之获取
Aug 28 Javascript
Javascript级联下拉菜单以及AJAX数据验证核心代码
May 10 Javascript
jquery ajax 调用失败的原因示例介绍
Sep 27 Javascript
jquery制作搜狐快站页面效果示例分享
Feb 21 Javascript
JavaScript返回网页中锚点数目的方法
Apr 03 Javascript
JavaScript中判断两个字符串是否相等的方法
Jul 07 Javascript
AngularJS学习笔记(三)数据双向绑定的简单实例
Nov 08 Javascript
JavaScript中使用Async实现异步控制
Aug 15 Javascript
vue+element 模态框表格形式的可编辑表单实现
Jun 07 Javascript
vue实现弹幕功能
Oct 25 Javascript
BootStrap前端框架使用方法详解
Feb 26 Javascript
Vue3.0 手写放大镜效果
Jul 25 Vue.js
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
PHP系统命令函数使用分析
2013/07/05 PHP
浅析PHP原理之变量(Variables inside PHP)
2013/08/09 PHP
php Calender(日历)代码分享
2014/01/03 PHP
xml 与javascript结合的问题解决方法
2007/03/24 Javascript
Flash+XML滚动新闻代码 无图片 附源码下载
2007/11/22 Javascript
javascript instanceof 与typeof使用说明
2010/01/11 Javascript
js 多种变量定义(对象直接量,数组直接量和函数直接量)
2010/05/24 Javascript
jQuery实现Div拖动+键盘控制综合效果的方法
2015/03/10 Javascript
JavaScript tab选项卡插件实例代码
2016/02/23 Javascript
基于Javascript实现返回顶部按钮
2016/02/29 Javascript
js实现按钮控制带有停顿效果的图片滚动
2016/08/30 Javascript
微信小程序 数据访问实例详解
2016/10/08 Javascript
关于JavaScript和jQuery的类型判断详解
2016/10/08 Javascript
使用微信内嵌H5网页解决JS倒计时失效问题
2017/01/13 Javascript
基于jQuery实现弹幕APP
2017/02/10 Javascript
原生js实现移动端触摸轮播的示例代码
2017/12/22 Javascript
react中fetch之cors跨域请求的实现方法
2018/03/14 Javascript
微信运维交互机器人的示例代码
2018/11/12 Javascript
详解javascript函数写法大全
2019/03/25 Javascript
jQuery使用jsonp实现百度搜索的示例代码
2020/07/08 jQuery
Vue中nprogress页面加载进度条的方法实现
2020/11/13 Javascript
Python爬取Coursera课程资源的详细过程
2014/11/04 Python
Java分治归并排序算法实例详解
2017/12/12 Python
快速解决vue.js 模板和jinja 模板冲突的问题
2019/07/26 Python
Django认证系统实现的web页面实现代码
2019/08/12 Python
深入学习python多线程与GIL
2019/08/26 Python
解决python彩色螺旋线绘制引发的问题
2019/11/23 Python
python关于调用函数外的变量实例
2019/12/26 Python
英国折扣零售连锁店:QD Stores
2018/12/08 全球购物
捐献物资倡议书范文
2014/05/19 职场文书
英语专业求职信
2014/07/08 职场文书
忠诚教育心得体会
2014/09/03 职场文书
信贷客户经理岗位职责
2015/04/09 职场文书
驻村工作简报
2015/07/20 职场文书
学习心得体会
2019/06/20 职场文书
解决vue中provide inject的响应式监听
2022/04/19 Vue.js