在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
则是在有修改的地方重新生成指令操作函数。因此消耗内存小,并且用不到的指令函数可以被优化掉。
参考资料: