酷播亮新聞
最棒的知識補給站

【譯】在Angular中使用DOM:意想不到的後果及優化技術

原文連結: Working with DOM in Angular: unexpected consequences and optimization techniques

我最近在NgConf上以講習班的形式講述了 Angular中的高階DOM操作 。我從基本的使用模板引用和DOM查詢等訪問DOM元素,一直講到使用view container動態地呈現模板和元件。如果你還沒有看過這個話題,我推薦你去看一下。通過一系列實踐練習,你將能夠更快地學習和強化新知識。我在NgViking上也有一個 關於此主題的較短演講

然而,如果你想要一個 TL;DR 版本或者只是想閱讀而不是聽講,我在本文中總結了關鍵概念。我將首先講解在Angular中使用DOM的工具和方法,然後繼續討論我在研討會期間未講的更高階的優化技術。

你可以在 這個github倉庫 中找到我在演講中使用的例子。

窺視檢視引擎

假設你有一個任務從DOM中刪除一個子元件。這是一個父元件的模板,帶有一個需要刪除的子元件 A

@Component({ ... template: `   ` }) export class AppComponent {} 

解決此任務的 不正確 方法是直接使用Renderer或原生DOM API刪除DOM元素

@Component({...}) export class AppComponent { ... remove() { this.renderer.removeChild( this.hostElement.nativeElement, // parent App comp node this.childComps.first.nativeElement // child A comp node ); } } 

你可以在 這裏 看到完整的解決方案。如果刪除節點後你在 Elements 選項卡中檢查生成的HTML,則會看到子元件 A 已經不在DOM中:

但是,如果你再檢查控制檯,Angular仍然會將子元件的數量報告為 1 而不是 0 。更糟糕的是,對於子元件 A 及其子元件 仍然執行 變更檢測。這是來自控制檯的日誌:

為什麼呢

發生這種情況是因為Angular在內部使用通常稱為 檢視元件檢視 的數據結構來表示元件。這是一個表示檢視和DOM之間關係的圖表:

每個檢視由檢視節點組成,這些檢視節點儲存對相應DOM元素的引用。因此,當我們直接更改DOM時,檢視內儲存對該DOM元素引用的檢視節點不受影響。這是一個圖表,顯示了從DOM中刪除 A 元件元素後的檢視和DOM的狀態:

由於所有變更檢測操作(包括在檢視上而 不是在DOM上執行 的子檢視),Angular都會檢測與 A 元件對應的檢視並輸出 1 ,而不是按預期的那樣輸出 0 。而且,由於與 A 元件相對應的檢視的存在,它也對 A 元件及其所有子元件進行變更檢測。

這表明,你不能直接從DOM中刪除子元件。事實上,你應該避免刪除由框架建立的任何HTML元素,只刪除Angular不知道的元素,這些可能是由你的程式碼或某個第三方外掛建立的元素。

爲了 正確地 解決這個問題,我們需要一個可以直接處理檢視的工具,在Angular中這個工具就是 View Container

View Container

view container對DOM層次結構的安全性進行了變動,並供Angular中的所有內建結構指令使用。它是一種特殊的檢視節點,它位於檢視內並充當其他檢視的容器:

如你所見,它可以包含兩種型別的檢視:嵌入檢視和宿主檢視。

這些是Angular中存在的唯一的檢視型別,它們主要不同之處是由用於建立它們的輸入資料決定。此外,嵌入檢視只能附屬於view container,而宿主檢視可以附屬到任何DOM元素(通常稱為宿主元素)。

嵌入檢視是使用 TemplateRef 從模板建立的,而宿主檢視是使用檢視(元件)工廠建立的。例如,用於引導應用程式的主要元件( AppComponent )在內部相當於附加到元件宿主元素( )的宿主檢視。

View Container提供API來建立,操作和刪除動態檢視。我將它們稱為動態檢視,而不是由模板中靜態元件建立的靜態檢視。 Angular不會為靜態檢視使用View Container,而是在子元件的特定節點內持有對子檢視的引用。下面的圖證明了這個想法:

如你所見,此處沒有View Container節點,並且對子檢視的引用直接附加到 A 元件檢視節點。

操作動態檢視

在開始建立並將檢視附加到view container之前,你需要將該容器引入元件的模板並初始化它。模板內的任何元素都可以充當view container,但是該角色最常見的候選物件是 ,因為它被呈現為註釋節點,因此不會向DOM引入冗餘元素。

要將任何元素轉換為view container,我們使用 {read:ViewContainerRef} 來進行檢視查詢:

@Component({ … template: `` }) export class AppComponent implements AfterViewChecked { @ViewChild('vc', {read: ViewContainerRef}) vc: ViewContainerRef; } 

一旦Angular完成檢視查詢評估並將對view container的引用分配給類的屬性,則可以使用該引用來建立動態檢視。

建立嵌入檢視

要建立 嵌入 檢視,需要一個模板。在Angular中,我們使用 元素包裹任何DOM元素來定義模板的結構。然後,我們可以簡單地使用帶有 {read:TemplateRef} 引數的檢視查詢來獲取對該模板的引用:

@Component({ ... template: `    ` }) export class AppComponent implements AfterViewChecked { @ViewChild('tpl', {read: TemplateRef}) tpl: TemplateRef; } 

一旦Angular評估完此查詢並將對模板的引用分配給類的屬性,我們就可以使用該引用來建立並使用 [createEmbeddedView](https://angular.io/api/core/ViewContainerRef#createEmbeddedView.) 方法將嵌入檢視附加到view container:

@Component({ ... }) export class AppComponent implements AfterViewInit { ... ngAfterViewInit() { this.viewContainer.createEmbeddedView(this.tpl); } } 

你應該在 ngAfterViewInit 生命週期鉤子內實現你的邏輯,因為這是在檢視查詢被初始化的時候。另外,對於嵌入檢視,你可以使用模板內繫結的值來定義上下文物件。檢視 API 文件可以獲取更多詳細資訊。

你可以在 這裏 找到建立嵌入檢視的完整示例。

建立宿主檢視

要建立宿主檢視,你需要一個元件工廠。想要了解更多關於工廠和動態元件檢查的資訊, 先了解這裏關於Angular中動態元件的知識

在Angular中,我們使用 [componentFactoryResolver](https://angular.io/api/core/ComponentFactoryResolver) 服務來獲取對元件工廠的引用:

@Component({ ... }) export class AppComponent implements AfterViewChecked { ... constructor(private r: ComponentFactoryResolver) {} ngAfterViewInit() { const factory = this.r.resolveComponentFactory(ComponentClass); } } } 

一旦我們獲得了元件的工廠,我們可以使用它來初始化元件,建立宿主檢視並將此檢視附加到view container。為此,我們只需呼叫 createComponent 方法並傳入一個元件工廠:

@Component({ ... }) export class AppComponent implements AfterViewChecked { ... ngAfterViewInit() { this.viewContainer.createComponent(this.factory); } } 

你可以在 這裏 找到建立宿主檢視的完整示例。

移除檢視

附加到view container的任何檢視都可以使用 removedetach 方法來刪除。這兩種方法都是從view container和DOM中移除一個檢視。但是, remove 方法會破壞檢視,導致以後不能被重新附著,但 detach 方法會保留它以便在將來重用,這對於我將在下面展示的優化技術非常重要。

因此,爲了正確地刪除子元件或任何DOM元素,首先需要建立一個嵌入或宿主檢視並將其附加到view container。這樣做後,你將能夠使用任何可用的API方法將其從view container和DOM中移除。

優化技術

有時你可能需要重複顯示和隱藏由模板定義的相同元件或HTML。在下面的例子中,通過點選不同的按鈕,切換元件顯示:

我們簡單地使用上面學到的方法,並用下面的程式碼來實現這一點:

@Component({...}) export class AppComponent { show(type) { ... // a view is destroyed this.viewContainer.clear(); // a view is created and attached to a view container this.viewContainer.createComponent(factory); } } 

每次單擊按鈕並執行 show 方法時,銷燬並重新建立檢視,這樣最終會產生不良後果。

在這個例子中,它是宿主檢視,因為我們使用了元件工廠和 createComponent 方法,所以它要被銷燬並重新建立。如果相反,我們使用 createEmbeddedView 方法和 TemplateRef ,嵌入檢視將被銷燬並重新建立:

show(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提供了一種將現有檢視附加到view container的方法,並在不銷燬它的情況下將其刪除。

ViewRef

ComponentFactoryTemplateRef 都實現了可用於建立檢視的檢視建立方法。實際上,當你呼叫 createEmbeddedViewcreateComponent 方法並傳入引數時,view container 在後臺就使用這些方法。好訊息是,我們可以自己呼叫這些方法來建立嵌入或宿主檢視並獲取對檢視的引用。在Angular中檢視使用 ViewRef 型別及其子型別進行引用。

建立宿主檢視

所以這就是你如何使用一個元件工廠來建立一個宿主檢視,並獲得對它的引用:

aComponentFactory = resolver.resolveComponentFactory(AComponent); aComponentRef = aComponentFactory.create(this.injector); view: ViewRef = aComponentRef.hostView; 

如果是 宿主 檢視,可以從 create 方法返回的 ComponentRef 中檢索與元件關聯的檢視。它通過類似命名的 hostView 屬性暴露出來。

一旦我們獲得了檢視,就可以使用 insert 方法將其附加到view container。如果你不想再顯示的另一個檢視可以使用 detach 方法刪除和保留(譯者注:原文是preserved,可以和上文提到的 detachremove 方法不同之處一起理解)。因此,切換元件的 優化方案 應該如下實現:

showView2() { ... // Existing view 1 is removed from a view container and the DOM this.viewContainer.detach(); // Existing view 2 is attached to a view container and the DOM this.viewContainer.insert(view); } 

再次注意,我們使用 detach 方法來保留檢視以備後用而不是 clearremove 。你可以在 這裏 找到完整的實現。

建立嵌入檢視

在基於模板建立嵌入檢視的情況下,檢視直接由 createEmbeddedView 方法返回:

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

和前面的示例類似,一個檢視可以從view container中移除,另一個檢視可以重新附加上去。你還是可以在 這裏 找到完整的實現。

有趣的是,檢視建立方法 createEmbeddedView 和view container的 createComponent 也返回對建立的檢視的引用。

如有侵權請來信告知:酷播亮新聞 » 【譯】在Angular中使用DOM:意想不到的後果及優化技術