【翻译】在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
这两个建立视图的方法,都会返回被建立的视图的引用。