初探Angular编译器的工作流程

Angular V13发布后,Ivy编译模式成为默认,View Engine退出历史舞台。

使用View Engine时,先生成ngSummary.js再生成ngFactory.js的模板编译方式不复存在了。Ivy模式下,template被转换成DOM抽象结构的指令操作函数,而如@Component等的装饰器信息被转化为静态属性。

本文是对Angular编译工具ngc的工作流程进行初步学习和总结。

回顾View Engine

原来的编译模式,在初始化时会首先收集全局的元数据信息,然后生成模板和指令的定义。

Snipaste_2022-04-08_14-36-16.png

首先收集元数据信息,包括selectorDependency、@Input和@Output等信息:

image.png

随后通过试图工厂函数返回template编译结果:

image.png

由于这种工作模式,库的元数据信息中的依赖项只能在构建时确定,导致库不能发布编译后产物,只能发布metadata数据,等到应用在构建时进行编译。

并且,被依赖方的改变也会导致依赖方的重新编译,因为依赖方的元数据信息需要重新生成,导致原来的factory也变成invalid

迎接Ivy

Ivymental modal:装饰器就是编译器。可以理解为作为传入class transfomer的参数来得到definition。。

通常情况,装饰器的上述转换过程仅需要自身的信息就足够了,除了一个例外:@Component。因为它具有template,而template可能会依赖selectorsdirectives and pipes,因此需要获取NgModuleDeclarationImport的信息。

初始化时会进行全局分析,建立import graphSemantic dependency graph ,确定依赖关系,解决了之前版本的历史问题:修改被依赖方的代码,不再会让依赖方也需要重新编译。

模板内容不再由factory生成,而是编译成直接的DOM抽象结构指令函数操作:没有用到的DOM指令函数可以被tree-shakingimage.png

ngc

Angular编译器ngc的主要任务有三个:

  1. 编译decorators,包括componenttemplate
  2. template应用类型检查
  3. 变化发生时进行高效率的re-compile

为了完成上述3个目标,ngc的工作流程由下面几步完成:

Step 1. 创建TypeScript编译器实例,并扩展Angular特性

TypeScript编译器中,待编译的程序被表示为ts.Program实例,包含了待编译的文件集合,依赖项的类型信息,和编译过程定义的参数。

通过入口文件遍历,得到所有的文件和依赖列表。不需要编译的依赖(如引用的库)会读取其.d.ts文件得到类型信息。

对于自己写的文件(my.component.ts),ngc会添加一个额外的.ngtypecheck后缀的文件(my.component.ngtypecheck.ts)到ts.Program来做内部的类型检查工作用。

对于特定的参数可能会有额外的编译结果,比如生成.ngfactory文件来做View Engine的向后扩展。

Step 2. 解析装饰器信息(Individual analysis)

将装饰器代码转换为对应的definition信息,包括templateselectorview encapsulation等。取决于decorator type

这就要求编译器具备读取表达式的能力(在不运行代码的情况下),比如:

const MY_SELECTOR = 'my-cmp'; 
@Component(
    { 
        selector: MY_SELECTOR, 
        template: '...', 
    }
) 
export class MyCmp {}
复制代码

编译器需要去解析MY_SELECTOR得到'my-cmp'。这也让开发者能更灵活的定义装饰器信息。

Step 3. 收集依赖信息,建立依赖关系(Global analysis)

完成了上一步的单独分析后,编译器需要确定各个decorator信息之间的依赖关系。组件在依赖其他组件时不需要直接导入,而是在template中使用类css classselector即可,这个建立依赖的工作是NgModule完成的。

因此,NgModule需要确定两个scope

  1. Compilation Scope:所有声明的和导入其他NgModule中声明的依赖
  2. Export Scope:导出的依赖

之后,ngc会建立两个graph

  1. import graph:所有依赖项的信息。
  2. Semantic dependency graph:依赖项之间的语意关联信息。

对于库等不需要编译的内容,会通过其所提供的.d.ts文件来获取类型信息作为dependencies。

Step 4. 模版类型检查

ngcTemplate转换成代表相同操作的类型层面的TypeScript Code(类型计算/类型体操),然后交给TypeScript判断是否有错误。

比如:

<span *ngFor="let user of users">{{user.name}}</span>
复制代码

会被转换成

import * as i0 from './test'; 
import * as i1 from '@angular/common'; 
import * as i2 from '@angular/core'; 

const _ctor1: <T = any, U extends i2.NgIterable<T> = any>(init: Pick<i1.NgForOf<T, U>, "ngForOf" | "ngForTrackBy" | "ngForTemplate">) => i1.NgForOf<T, U> = null!; 
/*tcb1*/ 
function _tcb1(ctx: i0.TestCmp) { 
    if (true) { 
    var _t1 /*T:DIR*/ /*165,197*/ = _ctor1({ 
            "ngForOf": (((ctx).users /*190,195*/) /*190,195*/) /*187,195*/, 
            "ngForTrackBy": null as any, 
            "ngForTemplate": null as any 
         }) 
     /*D:ignore*/; 
     _t1.ngForOf /*187,189*/ = (((ctx).users /*190,195*/) /*190,195*/) /*187,195*/; 
     var _t2: any = null!; 
     if (i1.NgForOf.ngTemplateContextGuard(_t1, _t2) /*165,216*/) { 
         var _t3 /*182,186*/ = _t2.$implicit /*178,187*/; "" + (((_t3 /*199,203*/).name /*204,208*/) /*199,208*/); 
         } 
     } 
 }
复制代码

(属实是看不懂)

Step 5. 完成

至此,ngc已经解析完程序并确保了没有fatal errors了,可以通过Typescript编译器来生成JavaScript代码了。这个过程中,decorator会被移除,多个静态属性会被添加到类的定义中:如selectorstyletemplatecomponentDefinition等。

export class MyComponent { 
    name: string; 
    static ɵcmp = core.ɵɵdefineComponent({ 
        type: HelloComponent, 
        tag: 'hello-component', 
        factory: () => new HelloComponent(), 
        template: function (rf, ctx) { 
            if (rf & RenderFlags.Create) { 
                core.ɵɵelementStart(0, 'div'); core.ɵɵtext(1); 
                core.ɵɵelementEnd(); 
                } 
            } 
        }); 
    }
复制代码

Step 6. Update

依据import graph对变化的部分进行重新编译;

依据Semantic dependency graph对上一步的变化结果进行分析,决定是否影响其他部分。

两个工具的联合使用帮助Angular完成Incremental的更新方式。

关于Incremental DomVirtual Dom更新方式的区别:

Virtual Dom在渲染时,通过Render函数生成新的Virtual Dom与旧的作比较,通过diff算法得到变化结果;

Incremental Dom则是在有修改的地方重新生成指令操作函数。因此消耗内存小,并且用不到的指令函数可以被优化掉。

参考资料:

  1. How the Angular Compiler Works
  2. 了解Angular Ivy: Incremental DOM 和 Virtual DOM

猜你喜欢

转载自juejin.im/post/7084355816949547021