Angular Renderer (渲染器)

閱讀 Angular 6/RxJS 最新教程,請訪問 前端修仙之路

Angular 其中的一個設計目標是使瀏覽器與 DOM 獨立。DOM 是複雜的,所以使組件與它分離,會讓咱們的應用程序,更容易測試與重構。另外的好處是,因爲這種解耦,使得咱們的應用可以運行在其它平臺 (好比:Node.js、WebWorkers、NativeScript 等)。前端

爲了可以支持跨平臺,Angular 經過抽象層封裝了不一樣平臺的差別。好比定義了抽象類 Renderer、Renderer2 、抽象類 RootRenderer 等。此外還定義瞭如下引用類型:ElementRef、TemplateRef、ViewRef 、ComponentRef 和 ViewContainerRef 等。node

本文的主要內容是分析 Angular 中 Renderer (渲染器),不過在進行具體分析前,咱們先來介紹一下平臺的概念。git

平臺

什麼是平臺

平臺是應用程序運行的環境。它是一組服務,能夠用來訪問你的應用程序和 Angular 框架自己的內置功能。因爲Angular 主要是一個 UI 框架,平臺提供的最重要的功能之一就是頁面渲染。github

平臺和引導應用程序

在咱們開始構建一個自定義渲染器以前,咱們來看一下如何設置平臺,以及引導應用程序。typescript

import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {BrowserModule} from '@angular/platform-browser';

@NgModule({
  imports: [BrowserModule],
  bootstrap: [AppCmp]
})
class AppModule {}

platformBrowserDynamic().bootstrapModule(AppModule);

如你所見,引導過程由兩部分組成:建立平臺和引導模塊。在這個例子中,咱們導入 BrowserModule 模塊,它是瀏覽器平臺的一部分。應用中只能有一個激活的平臺,可是咱們能夠利用它來引導多個模塊,以下所示:bootstrap

const platformRef: PlatformRef = platformBrowserDynamic();
platformRef.bootstrapModule(AppModule1);
platformRef.bootstrapModule(AppModule2);

因爲應用中只能有一個激活的平臺,單例的服務必須在該平臺中註冊。好比,瀏覽器只有一個地址欄,對應的服務對象就是單例。此外如何讓咱們自定義的 UI 界面,可以在瀏覽器中顯示出來呢,這就須要使用 Angular 爲咱們提供的渲染器。api

渲染器

什麼是渲染器

渲染器是 Angular 爲咱們提供的一種內置服務,用於執行 UI 渲染操做。在瀏覽器中,渲染是將模型映射到視圖的過程。模型的值能夠是 JavaScript 中的原始數據類型、對象、數組或其它的數據對象。然而視圖能夠是頁面中的段落、表單、按鈕等其餘元素,這些頁面元素內部使用 DOM (Document Object Model) 來表示。數組

Angular Renderer

RootRenderer瀏覽器

export abstract class RootRenderer {
  abstract renderComponent(componentType: RenderComponentType): Renderer;
}

Rendererapp

/**
 * @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.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;
}

那好,如今問題來了:

  • 何時建立 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);
  // 建立RootRenderer
  const 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) 等。

後面若是有時間的話,咱們會介紹如何自定義渲染器,有興趣的讀者,能夠先查閱 "參考資源" 中的連接。

參考資源

相關文章
相關標籤/搜索