对新的Angular编译器Ivy的研究

我认为编译器非常有趣 ,”我们今天出版的材料作者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, "div", _c0); i0.ɵE(1, "h1"); i0.ɵT(2); i0.ɵe(); i0.ɵE(3, "img", _c1); i0.ɵe(); i0.ɵe(); i0.ɵE(4, "h2"); i0.ɵT(5, "Here are some links to help you start: "); i0.ɵe(); 

常春藤正是通过这种JavaScript代码来转换组件模板。 这是在以前版本的编译器中完成的相同操作:


以前的Angular编译器产生的代码

有一种感觉,Ivy生成的代码要简单得多。 您可以尝试使用组件模板(该模板位于src/app/app.component.html ),再次进行编译,并查看对其所做的更改将如何影响生成的代码。

解析生成的代码


让我们尝试解析生成的代码,并确切地查看其执行的操作。 例如,让我们寻找有关诸如i0.ɵEi0.ɵ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

这项技术可以发现方法名称ɵTtext ,方法名称ɵ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()调用。

elementStarttext方法的第一个参数是一个数字,其值随每次调用而增加。 它可能表示某个数组中的索引,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   }, 

在此阶段,我们不使用模板函数提供的rfctf参数。 我们将回到他们身边。 但首先,让我们看一下如何在屏幕上显示我们的第一个自制组件。

首次申请


为了在屏幕上显示组件,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的新功能有何看法?

Source: https://habr.com/ru/post/zh-CN419995/


All Articles