[Angular]在Angular中和DOM打交道的正確姿式

原文連接: https://blog.angularindepth.com/working-with-dom-in-angular-unexpected-consequences-and-optimization-techniques-682ac09f6866html

介紹

文章裏有不少Angular中的術語,能夠參見這篇文章typescript

使用ViewContainerRef來操做Angular中的DOMbootstrap

  1. 組件視圖(Component View)api

  2. 宿主視圖(host view): Angular會對定義在bootstrap和entryComponents中的組件建立宿主視圖,每一個宿主視圖在調用.createComponent(factory)時負責建立組件視圖(Component View)安全

  3. 內嵌視圖(Embedded View): 內嵌視圖是由<ng-template></ng-template>元素聲明的。app

初探視圖引擎

假設如今你有這樣一個需求,須要從DOM中移除某個組件框架

@Component({
  ...
  template: ` <button (click)="remove()">Remove child component</button> <a-comp></a-comp> `
})
export class AppComponent {}
複製代碼

有個錯誤的方法就是用Renderer的removeChild()方法或者原生DOM API來移除<a-comp></a-comp>元素; 以下dom

// 這是一個錯誤示例!!!
import { AfterViewChecked, Component, ElementRef, QueryList, Renderer2, ViewChildren } from '@angular/core';

@Component({
  selector: 'app-root',
  template: ` <button (click)="remove()">Remove child component</button> <a-comp #c></a-comp> `
})
export class AppComponent implements AfterViewChecked {
  @ViewChildren('c', {read: ElementRef}) childComps: QueryList<ElementRef>;


  constructor(private hostElement: ElementRef, private renderer: Renderer2) {
  }

  ngAfterViewChecked() {
    console.log('number of child components: ' + this.childComps.length);
  }

  remove() {
    this.renderer.removeChild(
      this.hostElement.nativeElement,
      this.childComps.first.nativeElement
    );
  }
}
複製代碼

固然,在執行完remove()方法後,審查元素中這個組件天然是消失了,可是尷尬的是ngAfterViewChecked()生命週期鉤子中仍顯示子組件的個數是1,更爲尷尬的是這個組件的變動檢測也仍然在運行,這固然不是咱們要的結果。post

why

這樣的現象是由於Angular內部使用了一個View類或者ComponentView類來描述一個組件;每一個視圖都包括了不少與DOM元素所關聯的視圖節點,可是視圖到DOM之間的綁定的是單向的,也就是說修改View會影響到DOM渲染,可是對DOM的操做並不會影響到View或者ComponentVIew;性能

Angular的變動檢測都是綁定在View上的,而不是DOM上,因此天然會有上面的現象。

因而可知你不能從DOM層面去試圖刪除一個組件;事實上,你不該該刪除任何由框架自己生成的html元素。固然,那些由你本身的代碼或者第三方插件生成的元素你能夠隨意刪除。

View Container(視圖容器)

爲了解決這個問題,首先咱們要來了解一下視圖容器; 視圖容器的存在使得對DOM的操做變得高度安全,事實上Angular不少內置指令的實現也是依靠視圖容器完成的。這是View中一個特殊的View Node,一般是做爲其餘視圖的容器。

視圖容器中能夠放置Angular中有且僅有的兩種類型的視圖,內嵌式圖(embedded view)宿主視圖(host view),他倆的區別主要在於建立的時候傳遞進去的參數的不通;另外,內嵌視圖只能依附於視圖容器,宿主視圖不只能夠依附於視圖容器,也能夠依附於其餘宿主元素(DOM元素);

內嵌視圖是由模板經過TemplateRef建立的,宿主視圖一般是由視圖(組件)工廠建立的,舉個栗子,AppComponent就是一個宿主視圖,依附於這個組件的宿主元素<app></app>

動態控制視圖

建立一個內嵌式圖(embedded view)

要建立一個內嵌式圖(embedded view),首先咱們得有一個模板(template),在Angular中,咱們通常使用<ng-template></ng-template>標籤來包裹一些DOM元素,從而定義一個**模板(template)的結構。而後咱們就能夠經過@ViewChild獲取對這個模板(template)**的引用;

一旦Angular完成了對這個查詢的解析,咱們就能夠用createEmbeddedView()方法在**視圖容器(view container)**上建立一個內嵌視圖

import { Component, AfterViewInit, ViewChild, ViewContainerRef } from '@angular/core';

@Component({
  selector: 'app-test-dom',
  template: ` <ng-template #tpl let-name="name"> {{name}} <div>ng template works</div> </ng-template> `
})
export class TestDomComponent implements AfterViewInit {

  @ViewChild('tpl') tpl;

  constructor( private viewContainer: ViewContainerRef, ) { }

  ngAfterViewInit() {
    console.log(this.tpl);
    this.viewContainer.createEmbeddedView(this.tpl, {name: 'yyz'});
  }
}
複製代碼

建立內嵌視圖的邏輯應該放在AfterViewInit生命週期中執行,由於此時全部的視圖查詢才被初始化。固然,對內嵌視圖而言,你也能夠在建立的時候傳遞一個**上下文對象(context object)**用以模板內的的數據綁定;具體見上例中第二個參數{name: 'yyz'};此方法的詳細api參見https://angular.io/api/core/ViewContainerRef#createEmbeddedView

建立一個宿主視圖(host view)

要建立宿主視圖,咱們須要一個組件工廠(component factory),有關組件工廠的更多信息,能夠查看https://blog.angularindepth.com/here-is-what-you-need-to-know-about-dynamic-components-in-angular-ac1e96167f9e;

在Angular中,咱們使用componentFactoryResolver服務來獲取對一個組件工廠的引用;獲取這個組件的factory引用後,咱們就能用它來初始化這個組件、建立**宿主視圖(host view)**並把這個視圖附加到視圖容器上,只須要調用ViewContainerRef中的createComponent()方法,將組建工廠傳遞進去便可

// app.module.ts
@NgModule({
    ...
    entryComponents: [
        aComponent,
    ],
})

// app.component.ts
...
import { aCompoenent } from './a.component.ts';
@Component({ ... })
export class AppComponent implements AfterViewInit {
    ...
    constructor(private r: ComponentFactoryResolver) {}
    ngAfterViewInit() {
        const factory = this.r.resolveComponentFactory(aComponent);
        this.viewContainer.createComponent(factory);
    }
}
複製代碼

移除一個視圖

全部被添加在視圖容器上的視圖均可以經過remove()或者detach()方法來移除;這兩個方法都將視圖從視圖容器和DOM上移除。他倆的區別就在於:remove()方法會將這個視圖銷燬掉,從而之後不能再次使用,可是detach()會將這個視圖存儲起來。這也對下面要介紹的有關技術優化很是重要。

優化方法

有時候咱們會很頻繁的去渲染和隱藏相同的組件或者模板定義的html。若是咱們只是去簡單的把ViewContainerclear()而後createComponent(),或者ViewContainerclear()而後createEmbeddedView()。這樣的性能開銷是比較大的。

// bad code
@Component({...})
export class AppComponent {
    showHostView(type) {
        ...
        // a view is destroyed
        this.viewContainer.clear();

        // a view is created and attached to a view container 
        this.viewContainer.createComponent(factory);
    }
    showEmbeddedView(type) {
        ...
        // a view is destroyed
        this.viewContainer.clear();
        // a view is created and attached to a view container
        this.viewContainer.createEmbeddedView(this.tpl);
    }
}
複製代碼

理想狀況下,咱們應該只建立一次視圖,而後複用。而不是一次又一次的建立並銷燬。View Container提供了將已有視圖附加到視圖容器上和移除時不銷燬視圖的API。

ViewRef

ComponentFactoryTemplateRef都聲明瞭建立視圖的方法;事實上,當你在調用createEmbeddedView()createComponent()方法時,視圖容器也調用了這些方法來建立。固然咱們也能夠手動調用這些方法建立內嵌視圖或者宿主視圖,從而得到對視圖的引用。@angular/core中提供了ViewRef類來解決這個問題。

建立宿主視圖

經過組建工廠咱們能夠輕鬆的建立一個宿主視圖並獲取對它的引用;

// 經過組建工廠的create()方法建立
aComponentFactory = resolver.resolveComponentFactory(aComponent);
aComponentRef: ComponentRef = aComponentFactory.create(this.injector);
view: ViewRef = aComponentRef.hostView;

// 獲取視圖後,咱們就能夠在視圖容器上進行操做
showView() {
    ...
    // 使用detach()方法而不是clear()或者remove()方法,從而保存對視圖的引用
    this.viewContainer.detach();
    this.viewContainer.insert(view)
}
複製代碼

建立內嵌視圖

內嵌視圖是由模板建立出來的,createEmbeddedView()方法直接就返回了對視圖的引用;

import {AfterViewInit, Component, TemplateRef, ViewChild, ViewContainerRef, ViewRef} from '@angular/core';

  @Component({
    selector: 'app-root',
    template: ` <button (click)="show('1')">Show Template 1</button> <button (click)="show('2')">Show Template 2</button> <div> <ng-container #vc></ng-container> </div> <ng-template #t1><span>I am SPAN from template 1</span></ng-template> <ng-template #t2><span>I am SPAN from template 2</span></ng-template> `
  })
  export class AppComponent implements AfterViewInit {
    @ViewChild('vc', {read: ViewContainerRef}) vc: ViewContainerRef;
    @ViewChild('t1', {read: TemplateRef}) t1: TemplateRef<null>;
    @ViewChild('t2', {read: TemplateRef}) t2: TemplateRef<null>;
    view1: ViewRef;
    view2: ViewRef;

    ngAfterViewInit() {
      this.view1 = this.t1.createEmbeddedView(null);
      this.view2 = this.t2.createEmbeddedView(null);
    }

    show(type) {
      const view = type === '1' ? this.view1 : this.view2;
      this.vc.detach();
      this.vc.insert(view);
    }
  }

複製代碼

固然,不光光是組件工廠的create()方法和模板的createEmbeddedView()方法,一個視圖容器的createEmbeddedView()createConponent()方法也是能夠得到對視圖的引用的。

結束! 哈!

相關文章
相關標籤/搜索