在Angular中操纵DOM:意料之外的效果及优化手艺

【翻译】在Angular中操纵DOM:意料之外的效果及优化手艺

原文链接:
https://blog.angularindepth.c…

作者:
Max Koretskyi

译者:
而井

《在Angular中操纵DOM:意料之外的效果及优化手艺》

我最近在NgConf的一个钻研会上议论了Angular中的高等DOM操纵的话题。我从基本学问最先讲起,比方运用模版援用和DOM查询来访问DOM元素,一向谈到了运用视图容器来动态衬着模版和组件。假如你还没有看过这个演讲,我勉励你去看看。经由过程一系列的实践,你将能够疾速地学会新学问,并增强认知。关于这个话题,我在NgViking 也有一个简朴地说话。

然则,假如你以为谁人版本太长了(译者注:指演讲视频)不想看,或许比起听,你更喜好浏览,那末我在这篇文章总结了(演讲的)症结观点。起首,我会引见在Angular中操纵DOM的东西和要领,然后再引见一些我在钻研会上没有说过的、更高等的优化手艺。

你能够在这个GitHub堆栈中找到我演讲中运用过的样例。

窥伺视图引擎

假定你有一个要将一个子组件从DOM中移除的使命。这里有一个父组件,它的模块中有一个子组件A须要被移除:

@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中操纵DOM:意料之外的效果及优化手艺》

为何?

发作这类状态是由于,在Angular内部中,运用了一般称为View或Component View的数据构造来代表组件。这张图显现了视图和DOM之间的关联:

《在Angular中操纵DOM:意料之外的效果及优化手艺》

每一个视图都由持有对应DOM元素的视图节点所构成。所以,当我们直接修正DOM的时刻,视图内部的视图节点以及持有的DOM元素援用并没有被影响。这里有一张图能够展现在我们从DOM中移除组件A后,DOM和视图的状态:

《在Angular中操纵DOM:意料之外的效果及优化手艺》

而且由于一切的更改检测操纵和对子视图的包括,都是运转在视图中而不是DOM上,Angular检测与组件相干的视图,而且报告(译者注:组件数目)为1,而不是我们希冀的0。别的,由于与组件A相干的视图照旧存在,所以关于组件A及其子组件的更改检测操纵照旧会被实行。

要正确地处置惩罚这个题目,我们须要一个能直接处置惩罚视图的东西,在Angular中它就是视图容器View Container

视图容器View Container

视图容器能够保证DOM级别的更改的平安,在Angular中,它被一切内置的构造指令所运用。在视图内部有一种迥殊的视图节点范例,它饰演着其他视图容器的角色:

《在Angular中操纵DOM:意料之外的效果及优化手艺》

正如你所见的那样,它持有两种范例的视图:嵌入视图(embedded views)和宿主视图(host views)。

在Angular中只要这些视图范例,它们(视图)主要的差别取决于用什么输入数据来建立它们。而且嵌入视图只能附加(译者注:挂载)到视图容器中,而宿主视图能够被附加到任何DOM元素上(一般称其为宿主元素)。

嵌入视图能够运用TemplateRef经由过程模版来建立,而宿主视图得运用视图(组件)工场来建立。比方,用于启动顺序的主要组件AppComponent,在内部被当作为一个用来附加挂载组件宿主元素<app-comp>的宿主视图。

视图容器供应了用来建立、操纵和移除动态视图的API。我称它们为动态视图,是为了和那些由框架在模版中发明的静态组件所建立出来的静态视图做对照。Angular不会对静态视图运用视图容器,而是在子组件特定的节点内坚持一个对子视图的援用。这张图能够表明这个主意:

《在Angular中操纵DOM:意料之外的效果及优化手艺》

正如你所见,这里没有视图容器,子视图的援用是直接附加到组件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);
    }
}

你能够在这里找到建立宿主视图的样例代码。

移除视图

一个视图容器中的任何附加视图,都能够经由过程removedetach要领来删除。两个要领都邑将视图从视图容器和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,它供应了将已存在的视图附加到视图容器之上、移除视图却不烧毁视图的方法。

ViewRef

ComponentFactoryTemplateRef都完成了用来建立视图的建立要领。事实上,当你挪用createEmbeddedViewcreateComponent 要领并传入输入数据时,视图容器在底层内部运用了这些建立要领。有一个好消息就是我们能够本身挪用这些要领来建立一个嵌入或宿主视图、猎取视图的援用。在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要领来替代clearremove要领,为以后的复用坚持视图(的援用)。你能够在这里找到全部完成。

建立一个嵌入视图

在以一个模版为基本来建立一个嵌入视图的状态下,视图(援用)能够直接经由过程createEmbeddedView要领来返回:

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

与之前的例子相似,有一个视图将会从视图容器移除,别的一个视图将会被从新附加到视图容器之上。你能够在这里找到全部完成。

风趣的是,视图容器(译者注:ViewContainerRef范例)的createEmbeddedViewcreateComponent这两个建立视图的要领,都邑返回被建立的视图的援用。

    原文作者:而井
    原文地址: https://segmentfault.com/a/1190000017487067
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞