原文連接: https://blog.angularindepth.com/working-with-dom-in-angular-unexpected-consequences-and-optimization-techniques-682ac09f6866html
文章裏有不少Angular中的術語,能夠參見這篇文章typescript
使用ViewContainerRef來操做Angular中的DOMbootstrap
組件視圖(Component View)api
宿主視圖(host view): Angular會對定義在bootstrap和entryComponents中的組件建立宿主視圖
,每一個宿主視圖在調用.createComponent(factory)
時負責建立組件視圖(Component View)
安全
內嵌視圖(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
這樣的現象是由於Angular內部使用了一個View類或者ComponentView類來描述一個組件;每一個視圖都包括了不少與DOM元素所關聯的視圖節點,可是視圖到DOM之間的綁定的是單向的,也就是說修改View會影響到DOM渲染,可是對DOM的操做並不會影響到View或者ComponentVIew;性能
Angular的變動檢測都是綁定在View上的,而不是DOM上,因此天然會有上面的現象。
因而可知你不能從DOM層面去試圖刪除一個組件;事實上,你不該該刪除任何由框架自己生成的html元素。固然,那些由你本身的代碼或者第三方插件生成的元素你能夠隨意刪除。
爲了解決這個問題,首先咱們要來了解一下視圖容器; 視圖容器的存在使得對DOM的操做變得高度安全,事實上Angular不少內置指令的實現也是依靠視圖容器完成的。這是View中一個特殊的View Node,一般是做爲其餘視圖的容器。
視圖容器中能夠放置Angular中有且僅有的兩種類型的視圖,內嵌式圖(embedded view)和宿主視圖(host view),他倆的區別主要在於建立的時候傳遞進去的參數的不通;另外,內嵌視圖只能依附於視圖容器,宿主視圖不只能夠依附於視圖容器,也能夠依附於其餘宿主元素(DOM元素);
內嵌視圖是由模板經過TemplateRef建立的,宿主視圖一般是由視圖(組件)工廠建立的,舉個栗子,AppComponent就是一個宿主視圖,依附於這個組件的宿主元素<app></app>
;
要建立一個內嵌式圖(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
要建立宿主視圖,咱們須要一個組件工廠(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。
ComponentFactory
和TemplateRef
都聲明瞭建立視圖的方法;事實上,當你在調用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()
方法也是能夠得到對視圖的引用的。
結束! 哈!