Angular 视图详解

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 元素的引用:

因此可以确定:

  1. 视图和 DOM 不是一回事,直接删除 DOM 元素并不会影响视图
  2. 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 创建嵌入视图并插入,返回 EmbeddedViewRef
  • createComponent: 根据组件类创建一个 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 继承自 ViewRef
  • ComponentRef 的 hostView 属性是 ViewRef 类型

定义:

ts 复制代码
export declare abstract class ViewRef extends ChangeDetectorRef {
    // 销毁视图
    abstract destroy(): void;
   // 是否已销毁
    abstract get destroyed(): boolean;
    // 销毁时的 Hook
    abstract onDestroy(callback: Function): void;
}

Reference

相关推荐
T^T尚1 小时前
uniapp H5上传图片前压缩
前端·javascript·uni-app
出逃日志1 小时前
JS的DOM操作和事件监听综合练习 (具备三种功能的轮播图案例)
开发语言·前端·javascript
XIE3921 小时前
如何开发一个脚手架
前端·javascript·git·npm·node.js·github
山猪打不过家猪2 小时前
React(五)——useContecxt/Reducer/useCallback/useRef/React.memo/useMemo
前端·javascript·react.js
前端青山2 小时前
React事件处理机制详解
开发语言·前端·javascript·react.js
科技D人生2 小时前
Vue.js 学习总结(14)—— Vue3 为什么推荐使用 ref 而不是 reactive
前端·vue.js·vue ref·vue ref 响应式·vue reactive
对卦卦上心2 小时前
React-useEffect的使用
前端·javascript·react.js
练习两年半的工程师2 小时前
React的基本知识:事件监听器、Props和State的区分、改变state的方法、使用回调函数改变state、使用三元运算符改变state
前端·javascript·react.js
啵咿傲2 小时前
在React中实践一些软件设计思想 ✅
前端·react.js·前端框架
GIS好难学2 小时前
《Vue零基础入门教程》第二课:搭建开发环境
前端·javascript·vue.js·ecmascript·gis·web