我认为深入了解 Angular 的新 Ivy 渲染引擎的内部工作原理会很有用。在本文中,我希望保持较高的水平,但同时提供有关 Ivy 如何在内部组织其数据结构以专注于内存性能的重要见解。
当Ivy进行渲染时,它需要跟踪三种数据:模板、逻辑树和渲染树。为了简洁起见,在我们的许多数据结构中这三个概念被缩写为 T、L 和 R 前缀。
模板是源代码的解析版本。它包含以 Ivy 指令和有关组件/指令的元数据的形式呈现模板的指令。如果您可以在源代码中找到它,那么模板数据结构中也会存在相应的字段。无论其中的代码是否被执行,模板信息都存在。例如,即使条件为假,*ngIf 后面的模板仍将具有此模板信息。在ivy中,模板信息存储在TView(以及TData和TNode)数据结构中。这些数据结构共同提供了 Ivy 模板在运行时所需的所有静态信息。 “静态”这个词很重要,可以将其与下一个 Ivy 概念:逻辑视图区分开来。
逻辑视图(LView)表示模板(TView)的实例。我们使用“逻辑”一词来强调开发人员如何从逻辑角度思考应用程序。 ParentComponent 包含 ChildComponent。从逻辑角度来看,我们认为 ParentComponent 包含 ChildComponent,因此是“逻辑”名称。 “逻辑”一词与渲染树的最终概念形成对比。
渲染树是实际的 DOM 渲染树。它与上面的逻辑树不同,渲染树必须考虑内容投影。因为这种父/子关系并不像逻辑视图中那样简单。
让我们看一个例子来巩固这些概念。
@Component({
selector: ‘child’,
template: ‘<span>I am a child.</span>’
})
class ChildComponent {}
@Component({
selector: ‘parent’,
template: ‘
<div>
projected content:
<ng-content></ng-content>
</div>
’
})
class ParentComponent {}
@Component({
selector: ‘demo’,
template: `
<parent id=”p1">
<child id=”c1"></child>
</parent>
<child id=”c2"></child>
`
})
class DemoApp {}
应用程序加载后(但在引导之前),Ivy 将上述代码解析到 TView 中。
注意:或多或少,这里描述的很多内容都是为了性能而延迟执行的,而且这是伪代码而不是实际执行。以下片段是伪代码。
const tChildComponentView = new TView(
tData: [
new TElementNode(‘span’),
new TTextNode(‘I am a child.’),
],
…,
);
const tParentComponentView = new TView(
tData: [
new TElementNode(‘div’),
new TTextNode(‘projected content: ‘),
new TProjectionNode(),
],
…,
);
const tDemoAppView = new TView(
tData: [
new TElementNode(‘parent’, [‘id’, ‘p1’]),
new TElementNode(‘child’, [‘id’, ‘c1’]),
new TElementNode(‘child’, [‘id’, ‘c2’]),
],
…,
)
下一步是引导(或实例化)应用程序。实例化涉及从 TView 创建 LView 实例。
const lParentComponentView_p1 = new LView(
tParentComponentView,
new ParentComponent(…),
document.createElement(‘div’),
document.createText(‘projected content: ‘),
);
const lChildComponentView_c1 = new LView(
tChildComponentView,
new ChildComponent(…),
document.createElement(‘span’),
document.createText(‘I am a child.’),
);
const lChildComponentView_c2 = new LView(
tChildComponentView,
new ChildComponent(…),
document.createElement(‘span’),
document.createText(‘I am a child.’),
);
const lDemoAppView = new LView(
tDemoAppView,
new DemoApp(…),
document.createElement(‘parent’),
lParentComponentView_p1,
document.createElement(‘child’),
lChildComponentView_c1,
document.createElement(‘child’),
lChildComponentView_c2,
)
上图展示了TView和LView的区别。看ChildComponent,TView只有一个实例,但是LView有两个实例,因为ChildComponent被使用了两次。另一个关键区别是 LView 仅存储特定于该组件实例的数据,例如组件实例和关联的 DOM 节点。 TView 存储在组件的所有实例之间共享的信息,例如需要创建哪些 DOM 节点。
在上面的示例中,LView 显示为一个类(new LView(…)。)实际上,我们将其存储为数组([…]。)将 LView 存储为数组是出于内存性能原因。每个模板都会有不同数量的 DOM 节点和子组件/指令,将其存储在数组中是最有效的方式。
使用数组进行存储的含义是,不清楚事物存储在数组中的哪个位置。 TData 的目的是描述 LView 中每个位置存储的内容。因此 LView 本身不足以进行推理,因为它存储的值没有上下文来说明这些值代表什么。 TView 描述了组件需要什么,但它不存储实例信息。通过将 LView 和 TView 放在一起,Ivy 可以访问并推理 LView 中的值。 LView 存储值,而 TView 存储 LView 中值的含义。
为了简化上面的示例,LView 仅存储 DOM 节点。实际上,LView 还存储绑定、注入器、消毒器以及与视图状态相关的任何其他内容(在 TView/TData 中具有相应的条目。)
思考 TView 和 LView 的一种方法是将这些概念与面向对象编程联系起来。 TView就像类,LView就像类实例。
系统中的所有LView形成一棵树。对于我们的例子,我们可以像这样想象它。
<#LView>
<demo>
<#LView: tDemoAppView, new DemoApp()>
<parent id=”p1">
<#LView: tParentComponentView, new ParentComponent()>
<div>
projected content:
<ng-content></ng-content>
</div>
</#LView>
<child id=”c1">
<#LView: tChildComponentView, new ChildComponent()>
<span>
I am a child.
</span>
</#LView>
</child>
</parent>
<child id=”c2">
<#LView: tChildComponentView, new ChildComponent()>
<span>
I am a child.
</span>
</#LView>
</child>
</#LView>
</demo>
</#LView>
上面的“逻辑”树与下面所示的“渲染”树不同。主要区别在于,在“逻辑”树中,节点保持在一起,而在“渲染”树中,节点由于内容投影而分布。
<demo>
<parent id=”p1">
<div>
projected content:
<child id=”c1">
<span>
I am a child.
</span>
</child>
</div>
</parent>
<child id=”c2">
<span>
I am a child.
</span>
</child>
</demo>
我希望这篇文章能让您深入了解 Ivy 如何针对内存消耗进行优化。如果您喜欢这些类型的 Ivy 内部深入研究,请发表评论告诉我们。感谢您的阅读并感谢您成为 Angular 社区的一员。
原文链接:https://blog.angular.dev/ivys-internal-data-structures-f410509c7480