likes
comments
collection
share

Angular 应用程序中的变更检测和组件树

作者站长头像
站长
· 阅读数 26

我们可以将 Angular 应用程序视为组件树。在引擎盖下,对于组件,Angular 使用称为 View 的低级抽象。在本文中,我们将详细了解组件树如何对应于视图树. angular在 Web 应用程序中使用基于组件的方法,组合是通过在模板中包含子组件来实现的。

出于这个原因,我们可以将 Angular 应用程序视为组件树。然而,在幕后,对于组件,Angular 使用了一个称为 View 的低级抽象。它是创建和销毁的最小元素组。像属性检查和 DOM 更新这样的所有操作都是在view上执行的,因此在技术上更正确的说法是 Angular 是一棵view树,而组件可以描述为视图的更高级别概念。

关联数据结构的视图view

view的结构通过LView 接口定义。Lview存储处理指令所需的所有信息,因为它们是从模板调用的。每个组件和嵌入视图都有它对应的Lview。我们可以称创建组件的视图称为组件视图,以此来区别于模板引用(ng-template)、通过viewContainerRef创建的嵌入式视图。 使用 LView 上的指定属性跟踪视图的层次结构

export const NEXT = 4;
export const CHILD_HEAD = 13;
export const CHILD_TAIL = 14;
 
export interface LView {
  [CHILD_HEAD]: LView|LContainer|null;
  [CHILD_TAIL]: LView|LContainer|null;
  [PARENT]: LView|LContainer|null;
  [NEXT]: LView|LContainer|null;
}

要遍历视图树,Angular 使用这些遍历工具 Angular 还实现了 TView 数据结构,它为 LView 保存静态数据。这个 TView 在给定类型的所有 LView 之间共享。这意味着特定组件的每个实例都有自己的 LView 实例,但它们都引用同一个 TView 实例。

我们需要知道的最后一点是 Angular 有几个不同的视图类型定义如下:

export const enum TViewType {
  Root = 0,
  Component = 1,
  Embedded = 2,
}

Root 视图是 Angular 用于将顶级组件引导到其中的一种特殊类型的视图。 它与 LView 结合使用,LView 获取不属于 Angular 的现有 DOM 节点并将其包装在 LView 中,以便可以将其他组件加载到其中。

视图和组件之间存在直接联系,一个视图与一个组件相关联,反之亦然。视图在 CONTEXT 属性中保存对关联组件类实例的引用。属性检查和 DOM 更新等所有操作都在视图上执行。

对于包含两次 A 组件的模板,数据结构将如下所示:

Angular 应用程序中的变更检测和组件树

变更检测树

在大多数应用程序中,您都有一个主要的组件视图树,它以您在 index.html 中引用的组件开头。还有其他通过门户创建的根视图,主要用于模式对话框、工具提示等。这些是需要在主树的层次结构之外呈现的 UI 元素,主要是出于样式目的,例如,这样它们就不会受到 overflow:hidden 的影响。 Angular 将此类树的顶级元素保存在 ApplicationRef 的 _views 属性中。这些树被称为变更检测树,因为当 Angular 全局运行变更检测时会遍历它们。运行更改检测的 tick 方法遍历 _views 中的每棵树,并通过调用 detectChanges 方法对每个视图运行检查:

export class ApplicationRef {
  tick(): void {
    try {
      this._runningTick = true;
      for (let view of this._views) {
        view.detectChanges();
      }
      if (typeof ngDevMode === 'undefined' || ngDevMode) {
        for (let view of this._views) {
          view.checkNoChanges();
        }
      }
    } catch (e) { ... } finally { ... }
  }
}

您还可以看到 tick 在同一组视图上运行 checkNoChanges 方法。

添加动态视图到applicationRef

Angular 允许在 Angular 的变更检测树之外将组件呈现为独立的 DOM 元素。但是由于还需要检查这些视图,ApplicationRef 实现方法 attachView() 和 detachView() 来添加/删除独立视图以更改检测树。这有效地将这些视图添加到变更检测期间遍历的 _views 数组中。

让我们看看这个例子。我们有 M 组件,我们想动态实例化它,然后渲染到主 Angular 树外部的 DOM 中。我们是这样做的:

@Component({
  selector: 'l-cmp',
  template: 'L'
})
export class L {
  constructor(moduleRef: NgModuleRef<any>, appRef: ApplicationRef){
  const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(M);
  
  let newNode = document.createElement('div');
  newNode.id = 'placeholder';
  document.body.prepend(newNode);
  
  const ref = factory.create(moduleRef.injector, [], newNode);
  appRef.attachView(ref.hostView);
  }
}

@Component({
   selector: 'm-cmp',
   template: '{{title}}'
})
export class M {
  title = 'I am the component that was created dynamically';
}

接下来我们看下dom结构

Angular 应用程序中的变更检测和组件树

我们再看下view属性:

Angular 应用程序中的变更检测和组件树

我们可以使用控制台找出那些 RootViewRef 实例代表什么:

const CONTEXT = 8;
const CHILD_HEAD = 13;

const view_1 = appRef._views[0];
const view_2 = appRef._views[1];

view_1._lView[TVIEW].type // 0 - HostView
view_1._lView[CONTEXT].constructor.name // M

view_1._lView[CHILD_HEAD][TVIEW].type // 0 - HostView
view_1._lView[CHILD_HEAD][CONTEXT].constructor.name // M

view_2._lView[CONTEXT].constructor.name // AppComponent (RootView)
view_2._lView[TVIEW].type // 0 - HostView

view_2._lView[CHILD_HEAD][CONTEXT].constructor.name // AppComponent (ComponentView)
view_2._lView[CHILD_HEAD][TVIEW].type // 1 - ComponentView

view_2._lView[CHILD_HEAD][CHILD_HEAD][CONTEXT].constructor.name // L

下述图表将清楚的展示之间的关系:

Angular 应用程序中的变更检测和组件树

引导多个根组件

在angular中引导多个根组件是可能的,比如像这样:

@NgModule({
  declarations: [ AppComponent, AppRootAnother ],
  imports: [  BrowserModule ],
  bootstrap: [ AppComponent, AppRootAnother ]
})
export class AppModule {}

这将创建两个根视图和相应的 html 标签

Angular 应用程序中的变更检测和组件树 唯一要记住的是 index.html 应该包含两个选择器的标签:

<html lang="en">
<head>
  <meta charset="utf-8">
  <title>LearnAngular</title>
</head>
<body>
  <app-root></app-root>
  <app-root-another></app-root-another>
</body>
</html>

通过此设置,Angular 将创建两个独立的变更检测树。它们将在 ApplicationRef._views 下注册,当调用 ApplicationRef.tick() 函数时,Angular 将为两棵树运行更改检测。这实际上类似于使用 attachView。但是,它们仍然是单个 ApplicationRef 的一部分,因此它们将共享为 AppModule 定义的注入器。