在Angular中操作DOM:意料之外的结果及优化技术

栏目: 编程语言 · AngularJS · 发布时间: 7年前

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


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Clean Code

Clean Code

Robert C. Martin / Prentice Hall / 2008-8-11 / USD 49.99

Even bad code can function. But if code isn’t clean, it can bring a development organization to its knees. Every year, countless hours and significant resources are lost because of poorly written code......一起来看看 《Clean Code》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

html转js在线工具
html转js在线工具

html转js在线工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换