Angular 应用程序中的变更检测和组件树
我们可以将 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 组件的模板,数据结构将如下所示:
变更检测树
在大多数应用程序中,您都有一个主要的组件视图树,它以您在 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结构
我们再看下view属性:
我们可以使用控制台找出那些 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中引导多个根组件是可能的,比如像这样:
@NgModule({
declarations: [ AppComponent, AppRootAnother ],
imports: [ BrowserModule ],
bootstrap: [ AppComponent, AppRootAnother ]
})
export class AppModule {}
这将创建两个根视图和相应的 html 标签
唯一要记住的是 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 定义的注入器。
转载自:https://juejin.cn/post/7208910021970935868