在Angular V13发布后,Ivy编译模式成为默认,View Engine退出历史舞台。
使用View Engine时,先生成ngSummary.js再生成ngFactory.js的模板编译方式不复存在了。Ivy模式下,template被转换成DOM抽象结构的指令操作函数,而如@Component等的装饰器信息被转化为静态属性。
本文是对Angular编译工具ngc的工作流程进行初步学习和总结。
回顾View Engine
原来的编译模式,在初始化时会首先收集全局的元数据信息,然后生成模板和指令的定义。
首先收集元数据信息,包括selector、Dependency、@Input和@Output等信息:
随后通过试图工厂函数返回template编译结果:
由于这种工作模式,库的元数据信息中的依赖项只能在构建时确定,导致库不能发布编译后产物,只能发布metadata数据,等到应用在构建时进行编译。
并且,被依赖方的改变也会导致依赖方的重新编译,因为依赖方的元数据信息需要重新生成,导致原来的factory也变成invalid。
迎接Ivy
Ivy的mental modal:装饰器就是编译器。可以理解为作为传入class transfomer的参数来得到definition。。
通常情况,装饰器的上述转换过程仅需要自身的信息就足够了,除了一个例外:@Component。因为它具有template,而template可能会依赖selectors,directives and pipes,因此需要获取NgModule中Declaration和Import的信息。
初始化时会进行全局分析,建立import graph 和 Semantic dependency graph ,确定依赖关系,解决了之前版本的历史问题:修改被依赖方的代码,不再会让依赖方也需要重新编译。
模板内容不再由factory生成,而是编译成直接的DOM抽象结构指令函数操作:没有用到的DOM指令函数可以被tree-shaking。
ngc
Angular编译器ngc的主要任务有三个:
- 编译
decorators,包括component和template - 对
template应用类型检查 - 变化发生时进行高效率的
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信息,包括template,selector,view 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 class的selector即可,这个建立依赖的工作是NgModule完成的。
因此,NgModule需要确定两个scope:
Compilation Scope:所有声明的和导入其他NgModule中声明的依赖Export Scope:导出的依赖
之后,ngc会建立两个graph:
- import graph:所有依赖项的信息。
- Semantic dependency graph:依赖项之间的语意关联信息。
对于库等不需要编译的内容,会通过其所提供的.d.ts文件来获取类型信息作为dependencies。
Step 4. 模版类型检查
ngc将Template转换成代表相同操作的类型层面的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会被移除,多个静态属性会被添加到类的定义中:如selector,style,template,componentDefinition等。
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 Dom 和 Virtual Dom更新方式的区别:
Virtual Dom在渲染时,通过Render函数生成新的Virtual Dom与旧的作比较,通过diff算法得到变化结果;
Incremental Dom则是在有修改的地方重新生成指令操作函数。因此消耗内存小,并且用不到的指令函数可以被优化掉。
参考资料: