【翻譯】在Angular中操做DOM:意料以外的結果及優化技術node
原文連接: https://blog.angularindepth.c...
做者: Max Koretskyi
譯者: 而井
我最近在NgConf的一個研討會上討論了Angular中的高級DOM操做的話題。我從基礎知識開始講起,例如使用模版引用和DOM查詢來訪問DOM元素,一直談到了使用視圖容器來動態渲染模版和組件。若是你尚未看過這個演講,我鼓勵你去看看。經過一系列的實踐,你將能夠快速地學會新知識,並增強認知。關於這個話題,我在NgViking 也有一個簡單地談話。git
然而,若是你以爲那個版本太長了(譯者注:指演講視頻)不想看,或者比起聽,你更喜歡閱讀,那麼我在這篇文章總結了(演講的)關鍵概念。首先,我會介紹在Angular中操做DOM的工具和方法,而後再介紹一些我在研討會上沒有說過的、更高級的優化技術。github
你能夠在這個GitHub倉庫中找到我演講中使用過的樣例。typescript
假設你有一個要將一個子組件從DOM中移除的任務。這裏有一個父組件,它的模塊中有一個子組件A須要被移除:api
@Component({ ... template: ` <button (click)="remove()">Remove child component</button> <a-comp></a-comp> ` }) export class AppComponent {}
解決這個任務的一個錯誤的方法就是使用Renderer或者原生的DOM API來直接移除<a-comp> DOM 元素:緩存
@Component({...}) export class AppComponent { ... remove() { this.renderer.removeChild( this.hostElement.nativeElement, // parent App comp node this.childComps.first.nativeElement // child A comp node ); } }
你能夠在這裏看到整個解決方案(譯者注:樣例代碼)。若是你經過Element tab來審查移除節點以後的HTML結果,你將看到子組件A已經不存在DOM中了。安全
然而,若是你接着檢查一下控制檯,Angular依然報道子組件的數量爲1,而不是0。而且關於對子組件A及其子節點的變動檢測還在錯誤的運行着。這裏是控制檯輸出的日誌:數據結構
發生這種狀況是由於,在Angular內部中,使用了一般稱爲View或Component View的數據結構來表明組件。這張圖顯示了視圖和DOM之間的關係:app
每一個視圖都由持有對應DOM元素的視圖節點所組成。因此,當咱們直接修改DOM的時候,視圖內部的視圖節點以及持有的DOM元素引用並無被影響。這裏有一張圖能夠展現在咱們從DOM中移除組件A後,DOM和視圖的狀態:框架
而且因爲全部的變動檢測操做和對子視圖的包含,都是運行在視圖中而不是DOM上,Angular檢測與組件相關的視圖,而且報告(譯者注:組件數量)爲1,而不是咱們指望的0。此外,因爲與組件A相關的視圖依舊存在,因此對於組件A及其子組件的變動檢測操做依然會被執行。
要正確地解決這個問題,咱們須要一個能直接處理視圖的工具,在Angular中它就是視圖容器View Container。
視圖容器能夠保障DOM級別的變更的安全,在Angular中,它被全部內置的結構指令所使用。在視圖內部有一種特別的視圖節點類型,它扮演着其餘視圖容器的角色:
正如你所見的那樣,它持有兩種類型的視圖:嵌入視圖(embedded views)和宿主視圖(host views)。
在Angular中只有這些視圖類型,它們(視圖)主要的不一樣取決於用什麼輸入數據來建立它們。而且嵌入視圖只能附加(譯者注:掛載)到視圖容器中,而宿主視圖能夠被附加到任何DOM元素上(一般稱其爲宿主元素)。
嵌入視圖可使用TemplateRef經過模版來建立,而宿主視圖得使用視圖(組件)工廠來建立。例如,用於啓動程序的主要組件AppComponent,在內部被看成爲一個用來附加掛載組件宿主元素<app-comp>
的宿主視圖。
視圖容器提供了用來建立、操做和移除動態視圖的API。我稱它們爲動態視圖,是爲了和那些由框架在模版中發現的靜態組件所建立出來的靜態視圖作對比。Angular不會對靜態視圖使用視圖容器,而是在子組件特定的節點內保持一個對子視圖的引用。這張圖能夠代表這個想法:
正如你所見,這裏沒有視圖容器,子視圖的引用是直接附加到組件A的視圖節點上的。
在你開始建立一個視圖並將其附加到視圖容器以前,你須要引入組件模版的容器而且將其進行實例化。模版中的任何元素均可以充當視圖容器,不過,一般扮演這個角色的候選者是<ng-container>
,由於在它會渲染成一個註釋節點,因此不會給DOM帶來冗餘的元素。
爲了將任意元素轉化成一個視圖容器,咱們須要對一個視圖查詢使用{read: ViewContainerRef}
配置:
@Component({ … template: `<ng-container #vc></ng-container>` }) export class AppComponent implements AfterViewChecked { @ViewChild('vc', {read: ViewContainerRef}) viewContainer: ViewContainerRef; }
一旦Angular執行對應的視圖查詢並將視圖容器的的引用賦值給一個類的屬性,你就可使用這個引用來建立一個動態視圖了。
爲了建立一個嵌入視圖,你須要一個模版。在Angular中,咱們會使用<ng-template>
來包裹任意DOM元素和定義模版的結構。而後咱們就能夠簡單地用一個帶有 {read: TemplateRef}
參數的視圖查詢來獲取這個模版的引用:
@Component({ ... template: ` <ng-template #tpl> <!-- any HTML elements can go here --> </ng-template> ` }) export class AppComponent implements AfterViewChecked { @ViewChild('tpl', {read: TemplateRef}) tpl: TemplateRef<null>; }
一旦Angular執行這個查詢而且將模版的引用賦值給類的屬性後,咱們就能夠經過createEmbeddedView方法使用這個引用來建立和附加一個嵌入視圖到一個視圖容器中:
@Component({ ... }) export class AppComponent implements AfterViewInit { ... ngAfterViewInit() { this.viewContainer.createEmbeddedView(this.tpl); } }
你須要在ngAfterViewInit
生命週期中實現你的邏輯,由於視圖查詢是那時完成實例化的。並且你能夠給模版(譯者注:嵌入視圖的模版)中的值綁定一個上下文對象(譯者注:即模版上綁定的值隸屬於這個上下文對象)。你能夠經過查看API文檔來了解更多詳情。
你能夠在這裏找到建立嵌入視圖的整個樣例代碼。
要建立一個宿主視圖,你就須要一個組件工廠。若是你須要瞭解Angular中動態組件的話,點擊這裏能夠學習到更多關於組件工廠和動態組件的知識。
在Angular中,咱們可使用componentFactoryResolver這個服務來獲取一個組件工廠的引用:
@Component({ ... }) export class AppComponent implements AfterViewChecked { ... constructor(private r: ComponentFactoryResolver) {} ngAfterViewInit() { const factory = this.r.resolveComponentFactory(ComponentClass); } } }
一旦咱們獲得一個組件工廠,咱們就能夠用它來初始化組件,建立宿主視圖並將其視圖附加到視圖容器之上。爲了達到這一步,咱們只需簡單地調用createComponent
方法,而且傳入一個組件工廠:
@Component({ ... }) export class AppComponent implements AfterViewChecked { ... ngAfterViewInit() { this.viewContainer.createComponent(this.factory); } }
你能夠在這裏找到建立宿主視圖的樣例代碼。
一個視圖容器中的任何附加視圖,均可以經過remove
和detach
方法來刪除。兩個方法都會將視圖從視圖容器和DOM中移除。可是remove
方法會銷燬視圖,因此以後不能從新附加(譯者注:即從緩存中獲取再附加,不用從新建立),detach
方法會保持視圖的引用,以便將來能夠從新使用,這個對於我接下來要講的優化技術很重要。
因此,爲了正確地解決移除一個子組件或任意DOM元素這個問題,首先有必要建立一個嵌入視圖或宿主視圖,並將其附加到視圖容器上。而後你纔有辦法使用任何可用的API方法來將視圖從視圖容器和DOM中移除。
有時你須要重複地渲染和隱藏模版中定義好的相同組件或HTML。在下面這個例子中,經過點擊不一樣的按鈕,咱們能夠切換要顯示的組件:
若是咱們把以前學過的知識簡單地應用一下,那代碼將會以下所示:
@Component({...}) export class AppComponent { show(type) { ... // 視圖被銷燬 this.viewContainer.clear(); // 視圖被建立並附加到視圖容器之上 this.viewContainer.createComponent(factory); } }
最終,咱們會得一個不想要的結果:每當按鈕被點擊、show
方法被執行時,視圖都會被銷燬和從新建立。
在這個例子中,宿主視圖會由於咱們使用組件工廠和createComponent
方法,而銷燬和重複建立。若是咱們使用createEmbeddedView
方法和TemplateRef
,那嵌入視圖也會被銷燬和重複建立:
show(type) { ... // 視圖被銷燬 this.viewContainer.clear(); // 視圖被建立並附加到視圖容器之上 this.viewContainer.createEmbeddedView(this.tpl); }
理想情況下,咱們只需建立視圖一次,以後在咱們須要的時候複用它。有一個視圖容器的API,它提供了將已經存在的視圖附加到視圖容器之上、移除視圖卻不銷燬視圖的辦法。
ComponentFactory
和TemplateRef
都實現了用來建立視圖的建立方法。事實上,當你調用createEmbeddedView
和 createComponent
方法並傳入輸入數據時,視圖容器在底層內部使用了這些建立方法。有一個好消息就是咱們能夠本身調用這些方法來建立一個嵌入或宿主視圖、獲取視圖的引用。在Angular中,視圖能夠經過ViewRef
及其子類型來引用。
因此經過這樣,你可使用一個組件工廠來建立一個宿主視圖和獲取它的引用:
aComponentFactory = resolver.resolveComponentFactory(AComponent); aComponentRef = aComponentFactory.create(this.injector); view: ViewRef = aComponentRef.hostView;
在宿主視圖狀況下,視圖與組件的關聯(引用)能夠經過ComponentRef調用create
方法來獲取。經過一個hostView
屬性來暴露。
一旦咱們得到到這個視圖,它就能夠經過insert
方法附加到一個視圖容器之上。另一個你不想顯示的視圖能夠經過detach
方法來從視圖中移除並保持引用。因此能夠經過這樣來解決組件切換顯示問題:
showView2() { ... // 視圖1將會從視圖容器和DOM中移除 this.viewContainer.detach(); // 視圖2將會被附加於視圖容器和DOM之上 this.viewContainer.insert(view); }
注意,咱們使用detach
方法來代替clear
或remove
方法,爲以後的複用保持視圖(的引用)。你能夠在這裏找到整個實現。
在以一個模版爲基礎來建立一個嵌入視圖的狀況下,視圖(引用)能夠直接經過createEmbeddedView
方法來返回:
view1: ViewRef; view2: ViewRef; ngAfterViewInit() { this.view1 = this.t1.createEmbeddedView(null); this.view2 = this.t2.createEmbeddedView(null); }
與以前的例子相似,有一個視圖將會從視圖容器移除,另一個視圖將會被從新附加到視圖容器之上。你能夠在這裏找到整個實現。
有趣的是,視圖容器(譯者注:ViewContainerRef類型)的createEmbeddedView
和createComponent
這兩個建立視圖的方法,都會返回被建立的視圖的引用。