“
我认为编译器非常有趣 ,”我们今天出版的材料作者Uri Shaked说。 去年,他写
了一篇文章 ,
该文章讨论了对Angular编译器进行逆向工程并模拟了编译过程的某些阶段,这有助于理解该机制的内部结构的特征。 应当指出,本材料的作者通常所说的“编译器”称为“渲染引擎”。
当Uri听说发布了新版本的Angular编译器Ivy时,他立即想仔细研究一下,找出与旧版本相比有什么变化。 在这里,与以前一样,编译器将接收由Angular创建的模板和组件,这些模板和组件将转换为Chrome和其他浏览器可以理解的常规HTML和JavaScript代码。

如果将新版本的编译器与前一个版本进行比较,结果表明Ivy使用了摇树算法。 这意味着编译器会自动删除未使用的代码片段(这也适用于Angular代码),从而减小了项目包的大小。 另一个改进是,现在每个文件都是独立编译的,这减少了重新编译的时间。 简而言之,借助新的
编译器,我们可以获得更小的程序集,更快的项目重新编译,更简单的现成代码。
了解编译器的工作方式本身很有趣(至少该材料的作者希望如此),但这也有助于更好地了解Angular的内部机制。 这将导致“角度思考”技能的提高,从而使您可以更有效地将此框架用于Web开发。
顺便说一句,您知道为什么新的编译器被命名为Ivy吗? 事实是,这个词听起来像是字母“ IV”的组合,大声读出来,代表用罗马数字书写的数字4。 “ 4”是Angular编译器的第四代。
常春藤应用
常春藤仍处于密集开发过程中,此过程可以在
此处观察到。 尽管编译器本身尚不适合战斗使用,但它将使用的RendererV3抽象功能已经相当强大,并且已随Angular 6.x一起提供。
尽管常春藤还没有准备好,但我们仍然可以看看他的工作成果。 怎么做? 通过创建一个新的Angular项目:
ng new ivy-internals
之后,您需要通过
tsconfig.json
添加到新项目文件夹中的
tsconfig.json
文件中来启用Ivy:
"angularCompilerOptions": { "enableIvy": true }
最后,我们通过在新创建的项目文件夹中执行
ngc
命令来启动编译器:
node_modules/.bin/ngc
仅此而已。 现在,您可以检查位于
dist/out-tsc
的生成的代码。 例如,看一下
AppComponent
模板的以下片段:
<div style="text-align:center"> <h1> Welcome to {{ title }}! </h1> <img width="300" alt="Angular Logo" src="…"> </div>
这里有一些链接可以帮助您开始:
通过查看
dist/out-tsc/src/app/app.component.js
可以找到为此模板生成的代码:
i0.ɵE(0, , _c0); i0.ɵE(1, ); i0.ɵT(2); i0.ɵe(); i0.ɵE(3, , _c1); i0.ɵe(); i0.ɵe(); i0.ɵE(4, ); i0.ɵT(5, ); i0.ɵe();
常春藤正是通过这种JavaScript代码来转换组件模板。 这是在以前版本的编译器中完成的相同操作:
以前的Angular编译器产生的代码有一种感觉,Ivy生成的代码要简单得多。 您可以尝试使用组件模板(该模板位于
src/app/app.component.html
),再次进行编译,并查看对其所做的更改将如何影响生成的代码。
解析生成的代码
让我们尝试解析生成的代码,并确切地查看其执行的操作。 例如,让我们寻找有关诸如
i0.ɵE
和
i0.ɵT
类的调用含义的问题的答案。
如果您查看生成文件的开头,那么我们将找到以下表达式:
var i0 = require("@angular/core");
因此,
i0
只是Angular核心模块,所有这些都是Angular导出的函数。 Angular开发团队使用字母indicate表示某些方法仅旨在提供
内部框架
机制 ,也就是说,用户不应直接调用它们,因为在发布新版本的Angular时不能保证这些方法的API不变性(实际上,我会说他们的API几乎可以保证会发生变化)。
因此,所有这些方法都是Angular导出的私有API。 通过在VS Code中打开项目并分析工具提示,很容易弄清它们的功能:
VS代码中的代码分析即使在此处解析了JavaScript文件,VS Code也会使用TypeScript中的类型信息来识别调用签名并查找特定方法的文档。 如果在选择方法名称后使用Ctrl +单击组合键(在Mac上为Cmd +单击),我们发现该方法的真实名称为
elementStart
。
这项技术可以发现方法名称
ɵT
是
text
,方法名称
ɵe
是
ɵe
。 有了这些知识,我们就可以“翻译”生成的代码,将其转换为更易于阅读的代码。 这是此类“翻译”的一小部分:
var core = require("angular/core"); //... core.elementStart(0, "div", _c0); core.elementStart(1, "h1"); core.text(2); core. (); core.elementStart(3, "img", _c1); core.elementEnd(); core.elementEnd(); core.elementStart(4, "h2"); core.text(5, "Here are some links to help you start: "); core.elementEnd();
并且,正如已经提到的,此代码对应于HTML模板中的以下文本:
<div style="text-align:center"> <h1> Welcome to {{ title }}! </h1> <img width="300" alt="Angular Logo" src="…"> </div>
这里有一些链接可以帮助您开始:
在分析了所有这些之后,很容易注意到以下几点:
- 每个打开的HTML标记都有一个对
core.elementStart()
的调用。 - 结束标记对应于对
core.elementEnd()
调用。 - 文本节点对应于对
core.text()
调用。
elementStart
和
text
方法的第一个参数是一个数字,其值随每次调用而增加。 它可能表示某个数组中的索引,Angular在其中存储到创建的元素的链接。
第三个参数也传递给
elementStart
方法。 研究了以上材料后,我们可以得出结论,该参数是可选的,并且包含DOM节点的属性列表。 您可以通过查看
_c0
的值并发现它包含
div
元素的属性及其值的列表来验证这一点:
var _c0 = ["style", "text-align:center"];
NgComponentDef注意
到目前为止,我们已经分析了所生成代码的负责呈现组件模板的部分。 此代码实际上位于分配给
AppComponent.ngComponentDef
的较大代码
AppComponent.ngComponentDef
-静态属性,包含有关组件的所有元数据,例如CSS选择器,其更改检测策略(如果已指定)和模板。 如果您渴望冒险-现在,您可以独立地了解冒险的方式,尽管我们将在下面讨论。
自制常春藤
现在,我们大体上理解了生成的代码,现在,我们可以尝试使用Ivy使用的相同RendererV3 API从头开始创建我们自己的组件。
我们将要创建的代码将类似于编译器生成的代码,但是我们将使其变得易于阅读。
让我们从编写一个简单的组件开始,然后将其手动转换为类似于Ivy获得的代码:
import { Component } from '@angular/core'; @Component({ selector: 'manual-component', template: '<h2><font color="#3AC1EF">Hello, Component</font></h2>', }) export class ManualComponent { }
编译器将
@component
装饰器的
@component
作为
@component
,创建指令,然后将其全部布置为组件类的静态属性。 因此,为了模拟Ivy的活动,我们删除了
@component
装饰器,并将其替换为静态
ngComponent
属性:
import * as core from '@angular/core'; export class ManualComponent { static ngComponentDef = core.ɵdefineComponent({ type: ManualComponent, selectors: [['manual-component']], factory: () => new ManualComponent(), template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => {
我们通过调用
ɵdefineComponent
定义已编译组件的元数据。 元数据包括组件的类型(以前用于实现依赖项),将调用此组件的CSS选择器(在我们的示例中,
manual-component
是HTML模板中组件的名称),返回新实例的工厂。组件,然后是定义组件模板的函数。 该模板显示组件的视觉表示,并在组件的属性更改时对其进行更新。 为了创建此模板,我们将使用上面发现的方法:
ɵE
,
ɵe
和
ɵT
。
template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => { core.ɵE(0, 'h2'); // h2 core.ɵT(1, 'Hello, Component'); // core.ɵe(); // h2 },
在此阶段,我们不使用模板函数提供的
rf
或
ctf
参数。 我们将回到他们身边。 但首先,让我们看一下如何在屏幕上显示我们的第一个自制组件。
首次申请
为了在屏幕上显示组件,Angular导出了一个名为
ɵrenderComponent
的方法。 您需要做的就是检查
index.html
文件是否包含与元素选择器
<manual-component>
对应的HTML标签,然后将以下内容添加到文件末尾:
core.ɵrenderComponent(ManualComponent);
仅此而已。 现在,我们有一个最小的自制Angular应用程序,仅包含16行代码。 您可以在
StackBlitz上
试用完成的应用程序。
变更检测机制
因此,我们有一个可行的例子。 您可以添加交互性吗? 说,有什么有趣的事情,例如在这里使用Angular的变更检测系统?
更改组件,以便用户可以自定义欢迎文本。 也就是说,不是让组件始终显示文本
Hello, Component
,而是让用户更改
Hello
之后的文本部分。
我们首先添加
name
属性和一个方法来将该属性的值更新到组件类:
export class ManualComponent { name = 'Component'; updateName(newName: string) { this.name = newName; }
尽管这一切看起来并不特别令人印象深刻,但最有趣的是未来。
接下来,我们将编辑模板函数,以使它显示
name
属性的内容,而不是不可变的文本:
template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => { if (rf & 1) { // : core.ɵE(0, 'h2'); core.ɵT(1, 'Hello, '); core.ɵT(2); // <-- name core.ɵe(); } if (rf & 2) { // : core.ɵt(2, ctx.name); // ctx - } },
您可能已经注意到,我们将模板指令包装在检查
rf
值的
if
中。 Angular使用此参数来指示是否是第一次创建组件(将
设置最低有效位),或者我们只需要在检测更改的过程中更新动态内容(这是第二个
if
的目标)。
因此,当首次显示组件时,我们创建所有元素,然后,当检测到更改时,我们仅更新可能更改的内容。
ɵt
内部方法对此负责(请注意小写字母
t
),它对应于
textBinding
导出的
textBinding
函数:
函数textBinding因此,第一个参数是要更新的元素的索引,第二个参数是值。 在这种情况下,我们使用命令
core.ɵT(2);
创建一个索引为2的空文本元素
core.ɵT(2);
。 它充当
name
的占位符。 我们使用命令
core.ɵt(2, ctx.name);
对其进行更新
core.ɵt(2, ctx.name);
在检测到相应变量的变化时。
此刻,尽管我们可以更改
name
属性的值,但此组件的输出仍将显示文本
Hello, Component
,这将导致屏幕上的文本发生更改。
为了使应用程序真正实现交互,我们将在此处添加带有事件侦听器的数据输入字段,该事件侦听器调用组件方法
updateName()
:
template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => { if (rf & 1) { core.ɵE(0, 'h2'); core.ɵT(1, 'Hello, '); core.ɵT(2); core.ɵe(); core.ɵT(3, 'Your name: '); core.ɵE(4, 'input'); core.ɵL('input', $event => ctx.updateName($event.target.value)); core.ɵe(); }
事件绑定在行
core.ɵL('input', $event => ctx.updateName($event.target.value));
执行
core.ɵL('input', $event => ctx.updateName($event.target.value));
。 即,
ɵL
方法负责设置事件侦听器以获取最声明的元素。 第一个参数是事件的名称(在这种情况下,
input
是
<input>
元素的内容更改时引发的事件),第二个参数是回调。 此回调接受事件数据作为参数。 然后,我们从事件的目标元素(即
<input>
元素)中提取当前值,并将其传递给组件中的函数。
上面的代码等效于在模板中编写以下HTML:
Your name: <input (input)="updateName($event.target.value)" />
现在,您可以编辑
<input>
元素的内容,并观察组件中文本的变化。 但是,加载组件时不会填充输入字段。 为了使所有内容都能以这种方式工作,您需要在模板功能代码中再添加一条指令,该指令在检测到更改时执行:
template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => { if (rf & 1) { ... } if (rf & 2) { core.ɵt(2, ctx.name); core.ɵp(4, 'value', ctx.name); } }
在这里,我们使用渲染系统的另一种内置方法
ɵp
,该方法使用给定索引更新元素的属性。 在这种情况下,将索引4传递给该方法,该方法是分配给
input
元素的索引,我们
ctx.name
方法将
ctx.name
值放入此元素的
value
属性中。
现在我们的例子终于准备好了。 我们使用Ivy渲染系统API从零开始实现了双向数据绑定。 太好了
在这里,您可以尝试完成的代码。
现在,我们熟悉了新的Ivy编译器的大多数基本构建块。 我们知道如何创建元素和文本节点,如何绑定属性和配置事件侦听器以及如何使用变更检测系统。
关于* ngIf和* ngFor块
在完成常春藤研究之前,让我们看一下另一个有趣的话题。 即,让我们谈谈编译器如何使用子模式。 这些是用于
*ngIf
或
*ngFor
。 它们以特殊方式处理。 让我们看看如何在我们的自制模板代码中使用
*ngIf
。
首先,您需要安装npm软件包
@angular/common
*ngIf
地方。 接下来,您需要从此包中导入指令:
import { NgIf } from '@angular/common';
现在,为了能够在模板中使用
NgIf
,您需要为其提供一些元数据,因为
@angular/common
模块不是使用Ivy编译的(至少在编写材料时,将来可能会从
ngcc的介绍)。
我们将使用与熟悉的
ɵdefineComponent
方法相关的
ɵdefineComponent
方法。 它为指令定义元数据:
(NgIf as any).ngDirectiveDef = core.ɵdefineDirective({ type: NgIf, selectors: [['', 'ngIf', '']], factory: () => new NgIf(core.ɵinjectViewContainerRef(), core.ɵinjectTemplateRef()), inputs: {ngIf: 'ngIf', ngIfThen: 'ngIfThen', ngIfElse: 'ngIfElse'} });
我在
Angular源代码中找到了这个定义以及
ngFor
。 现在我们已经准备
NgIf
Ivy中使用
NgIf
,可以将以下内容添加到该组件的指令列表中:
static ngComponentDef = core.ɵdefineComponent({ directives: [NgIf], // ... });
接下来,我们仅为以
*ngIf
为边界的分区定义子
*ngIf
。
假设您需要显示一张图片。 让我们在模板函数中为此模板设置一个新函数:
function ifTemplate(rf: core.ɵRenderFlags, ctx: ManualComponent) { if (rf & 1) { core.ɵE(0, 'div'); core.ɵE(1, 'img', ['src', 'https://pbs.twimg.com/tweet_video_thumb/C80o289UQAAKIqp.jpg']); core.ɵe(); } }
该模板功能与我们已经编写的模板功能没有什么不同。 它使用相同的构造在
div
元素内创建
img
元素。
最后,我们可以通过将
ngIf
指令添加到组件模板来将它们放在一起:
template: (rf: core.ɵRenderFlags, ctx: ManualComponent) => { if (rf & 1) { // ... core.ɵC(5, ifTemplate, null, ['ngIf']); } if (rf & 2) { // ... core.ɵp(5, 'ngIf', (ctx.name === 'Igor')); } function ifTemplate(rf: core.ɵRenderFlags, ctx: ManualComponent) { // ... } },
请注意在代码的开头对新方法的调用(
core.ɵC(5, ifTemplate, null, ['ngIf']);
)。 它声明了一个新的容器元素,即具有模板的元素。 第一个参数是元素的索引,我们已经看到了这样的索引。 第二个参数是我们刚刚定义的子模式函数。 它将用作容器元素的模板。 第三个参数是元素的标签名称,在这里没有意义,最后还有与此元素相关联的指令和属性的列表。 这是
ngIf
来源。
在
core.ɵp(5, 'ngIf', (ctx.name === 'Igor'));
行中
core.ɵp(5, 'ngIf', (ctx.name === 'Igor'));
通过将
ngIf
属性绑定到逻辑表达式
ctx.name === 'Igor'
的值来更新元素的状态。 这将检查组件的
name
属性是否等于
Igor
。
上面的代码等效于以下HTML代码:
<div *ngIf="name === 'Igor'"> <img align="center" src="..."> </div>
在这里可以注意到,新的编译器不会生成最紧凑的代码,但是与现在的代码相比,它还不错。
您可以
在此处尝试一个新示例。 要查看正在使用的
NgIf
部分,请在
Your name
字段中输入名称
Igor
。
总结
我们几乎遍历了Ivy编译器的功能。 希望这次旅行激发了您对Angular进一步探索的兴趣。 如果是这样,那么现在您拥有了尝试Ivy所需的一切。 现在您知道了如何将模板“转换”为JavaScript,如何在不使用此编译器的情况下访问Ivy使用的相同Angular机制。 我想所有这些都将使您有机会深入探索新的Angular机制。
在这里 ,
这里和
这里 -三种材料,您可以在其中找到有关常春藤的有用信息。
这是Render3的源代码。
亲爱的读者们! 您对Ivy的新功能有何看法?
