Angular 视图
什么是视图
视图(View)是DOM的抽象层,它屏蔽了不同平台渲染DOM的差异,给Angular提供了跨平台的能力。如果了解 React 框架,可以把视图理解为其中的 Virtual DOM。
Angular 在进行变更检测时,它检测的是视图。 在视图创建以后,Angular 框架不会对 DOM 元素有直接操作,所有的操作都是通过视图完成。对于模板中的每一个 DOM 元素,Angular 都会创建一个与之对应的视图,每个视图中包含对原始 DOM 元素的引用。
视图和 DOM 元素的关系如下:
一个例子理解 View 和 DOM 的关系
创建一个组件,其中包含一个 span
和一个 button
,点击 button
时删除 span
元素。 通过 @ViewChild
获取 span
元素对应的视图,它的类型为 ElementRef
,可以通过 nativeElement
属性获取其对应的 DOM 元素的引用。
ts
@Component({
selector: 'test-view',
template: `
<span #span> span element </span>
<button (click)="removeSpan()">remove span</button>
`,
})
export class TestComponent {
@ViewChild('span') spanEle: ElementRef;
constructor(private host: ElementRef, private render: Renderer2) {}
ngAfterViewChecked() {
console.log(this.spanEle);
}
removeSpan() {
this.render.removeChild(
this.host.nativeElement,
this.spanView.nativeElement
);
console.log('removed span');
}
}
在变更检测钩子中,打印视图对象 spanEle
,观察一下它在删除 span
元素前后的值。 @ViewChild
获取的视图变量在每次变更之后都会更新,从结果可以看到,使用 DOM 操作删除节点以后,页面上的 span
元素消失了,但是视图中仍然保存着对 span
元素的引用:
因此可以确定:
- 视图和 DOM 不是一回事,直接删除 DOM 元素并不会影响视图
- Angular 的变更检测时基于视图的
不要直接对 DOM 元素进行结构性操作,特别是删除元素,因为视图中仍然后保存着对 DOM 元素的引用,不仅会造成内存泄漏,而且由于每次变更检测时仍会检测被删除的元素的视图树,也会降低性能。对于这种情况,应该使用 View Container 解决。
视图的抽象类
上面的例子展示了 ElementRef 这一个视图抽象类。Angular 的视图抽象层提供了很多的抽象类,接下来会介绍最常见的几个。
ElementRef
最基本的抽象类,它只包含所引用的 DOM 元素对象。
ElementRef 的定义:
ts
export class ElementRef<T = any> {
constructor(nativeElement: T);
nativeElement: T; // DOM 元素的引用
}
使用 @ViewChild
装饰的原生 DOM 元素会返回 ElementRef
,如上述例子中的 spanEle
。 同时,由于组件都挂载在自定义 DOM 元素上,所以可以使用依赖注入可以获取组件挂载的自定义元素的 ElementRef
,它会作为组件视图的容器,如上述例子中 host
。
在 Angular 的官方文档和其他技术文档中,语言上并没有严格区分 DOM 元素 (DOM elements) 和它的视图抽象 ElementRef,一般都使用 "element" 一词指代 ElementRef。 "视图 Views" 一词仅狭义地指下文中的嵌入视图、宿主视图和视图容器。这可能是因为 ElementRef 仅仅是一个简单的 DOM 元素的 wrapper。 但是还是要记住,ElementRef 也是 Angular 整个视图抽象层 的一部分,Angular 检测和操作的是 ElementRef 而不是 DOM。 参考官方文档 angular.cn/guide/gloss...
ViewContainerRef
视图容器,它存在于一个已有的视图中,可以挂载一个或多个子视图。 任何 DOM 元素都可以作为视图容器,只要在使用 @ViewChild
装饰元素时将 read
参数设置成 ViewContainerRef
就可以获得一个视图容器。更一般的做法是让 ng-container
作为视图容器,因为 ng-container
元素会在渲染时会被渲染成注释。
获取视图容器:
ts
@Component({
selector: 'sample',
template: `
<ng-container #container></ng-container>
`
})
export class SampleComponent implements AfterViewInit {
@ViewChild("container", {read: ViewContainerRef}) container: ViewContainerRef;
}
在视图容器上挂载的视图有两种:嵌入视图 (embedded view) 和宿主视图 (host view):
嵌入视图只能被挂载在视图容器中,它一般由 TemplateRef
创建
- 宿主视图可以被挂载在任何元素(
ElementRef
)上。它由组件类的视图工厂方法 (view factory) 创建。一般挂载在组件自身的 host element 上,比如AppComponent
的视图挂载在<app-root>
上
视图容器支持多个视图操作API,定义如下:
ts
// 创建视图的方法有多个签名,这里省略了参数
export abstract class ViewContainerRef {
// 清除容器中的所有视图
abstract clear(): void;
// 创建一个组件的视图并插入容器
abstract createComponent<C>(...): ComponentRef<C>;
// 创建一个嵌入视图并插入容器
abstract createEmbeddedView<C>(...): EmbeddedViewRef<C>;
// 卸载一个视图
abstract detach(index?: number): ViewRef | null;
// 获取视图容器的Anchor元素(挂载元素)
abstract get element(): ElementRef;
// 根据索引获取视图
abstract get(index: number): ViewRef | null;
// 获取指定视图的索引
abstract indexOf(viewRef: ViewRef): number;
// 获取注入器
abstract get injector(): Injector;
// 在指定位置插入视图
abstract insert(viewRef: ViewRef, index?: number): ViewRef;
// 返回视图数量
abstract get length(): number;
// 移动视图位置
abstract move(viewRef: ViewRef, currentIndex: number): ViewRef;
// 根据索引删除视图
abstract remove(index?: number): void;
}
两个创建视图的API :
createEmbeddedView
: 根据 TemplateRef 创建嵌入视图并插入,返回 EmbeddedViewRefcreateComponent
: 根据组件类创建一个ComponentRef
,并插入它的宿主视图ComponentRef.hostView
,返回ComponentRef
ComponentRef
通过视图容器的 createComponent
方法可以创建一个宿主视图。
底层使用
ComponentFactory
工厂方法创建,Angular 标记这种方式过时了,不建议开发者直接使用,这里就不展开了,在参考资料中有阐述。
创建宿主视图:
ts
@Component({
selector: 'test-view',
template: `
<button>click</button>
<ng-container #container></ng-container>
`,
})
export class TestComponent {
@ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;
ngAfterViewInit() {
const ref = this.container.createComponent(AnotherComponent);
console.log(ref);
}
}
页面中 AnotherComponent
被渲染,查看控制台,输出的类型为 componentRef
,宿主视图是它的一个属性:
除了宿主视图,ComponentRef
上还有其他有用的属性,比如 instance
组件实例,可以从上面获取实例的属性和方法。
ComponentRef 的定义:
ts
export declare abstract class ComponentRef<C> {
// host element
abstract get location(): ElementRef;
// 注入器
abstract get injector(): Injector;
// 组件实例
abstract get instance(): C;
// 模板的宿主视图
abstract get hostView(): ViewRef;
// change detector
abstract get changeDetectorRef(): ChangeDetectorRef;
// 创建视图的类
abstract get componentType(): Type<any>;
// 是否已销毁
abstract destroy(): void;
// 销毁时的 Hook
abstract onDestroy(callback: Function): void;
}
TemplateRef
TemplateRef 是 ng-template
元素的视图类。 ng-template
用于给一组 DOM 元素创建一个模版,它可以用模板变量引用。简单说就是给一组 DOM 命名,这些 DOM 元素不会被立即创建,可以被动态创建。 使用 @ViewChild
装饰模板变量时,会返回一个 TemplateRef
。
TemplateRef 的定义:
ts
export abstract class TemplateRef<C> {
abstract createEmbeddedView(context: C, injector?: Injector): EmbeddedViewRef<C>;
abstract readonly elementRef: ElementRef;
}
createEmbeddedView
方法用于根据 ng-template 中的元素创建嵌入式视图elementRef
是嵌入式视图所要挂载的元素的 ElementRef
TemplateRef
创建嵌入视图并插入到视图容器中:
ts
@Component({
selector: 'test-view',
template: `
<button>click</button>
<ng-container #container> </ng-container>
<ng-template #tpl> <span> in template</span> </ng-template>
`,
})
export class TestComponent {
@ViewChild('tpl') tpl: TemplateRef;
@ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;
ngAfterViewInit() {
this.container.insert(this.tpl.createEmbeddedView());
// 也可以直接使用视图容器的方法创建和插入
// this.container.createEmbeddedView(this.tpl)
}
}
页面的元素如下,嵌入视图被插入并渲染:
ng-if 指令的底层就是上述过程,Angular 会给 if 和 else 对应的 DOM 元素块分别创建template,根据条件表达式的结果,创建和插入嵌入视图
ViewRef
ViewRef 就是最基本的视图类。
- 基类是 ChangeDetectorRef
- 视图容器中的每一个视图都是 ViewRef
EmbeddedViewRef
继承自 ViewRefComponentRef
的 hostView 属性是 ViewRef 类型
定义:
ts
export declare abstract class ViewRef extends ChangeDetectorRef {
// 销毁视图
abstract destroy(): void;
// 是否已销毁
abstract get destroyed(): boolean;
// 销毁时的 Hook
abstract onDestroy(callback: Function): void;
}