Angular编译机制
这是我用来进行实验的代码,它是基于quickstart项目,并根据aot文档修改得到的。各位可以用它来进行探索,也可以自己基于quickstart进行修改(个人建议后者)。
为什么Angular需要编译
- 对于Angular来说,简练的js代码执行起来不高效,高效的js代码写起来不简练(详见参考资料5)。为了能够同时让Angular既易于书写又能有很高的效率,我们可以先用一种简练的Angular语法表达我们语义,然后让编译器根据我们写的源代码编译出真正用来执行的js代码。
- 编译可以让Angular与客户端(浏览器)解耦。也就是说,可以用另一种编译器,输入相同的Angular项目代码,输出的是能在手机上运行的应用!Angular首页就是这样介绍的:"Learn one way to build applications with Angular and reuse your code and abilities to build apps for any deployment target. For web,mobile web,native mobile and native desktop."
- Angular项目由很多组件组成,每个组件有自己的HTML模板,它们按照Angular规定的语法进行组织。然而Angular的语义并不能被浏览器直接理解。为了让浏览器能运行我们写的项目,这些组件和HTML模板必须先被Angular编译器编译成浏览器可执行的Javascript。
Angular文档:There is actually only one Angular compiler. The difference between AOT and JIT is a matter of timing and tooling.
Angular编译有两种:Ahead-of-time (AOT) 和 just-in-time (JIT)。但是实际上使用的是同一个编译器,AOT和JIT的区别只是编译的时机和编译所使用的工具库。
just-in-time (JIT)
JIT一般经历的步骤:
- 用Typescript和Angular语法写出源代码。
- 用
tsc
将Typescript代码(包括我们写的,以及Angular本身)编译成JavaScript代码。 - 打包、混淆、压缩。
- 将得到的bundle以及其他需要的静态资源部署到服务器上。
以下是发生在客户端(用户浏览器)的步骤: - 客户端下载bundle,开始执行这些JavaScript。
-
Angular启动,Angular调用编译器将Angular源代码(Javascript代码)编译成浏览器真正执行的Javascript目标代码(也就是后面会讲的
NgFactories
)。> Angular的启动源于main.js(由main.ts编译得到)的执行。
- 创建各种组件的实例(通过
NgFactories
),产生了我们看到的应用。
Ahead-of-time (AOT)
AOT一般经历的步骤:
- 用Typescript和Angular语法写出源代码。
-
用
ngc
(Angular compiler的command-line interface)编译应用,其中包括两步:- 2.1 将Angular源代码(此时是Typescript代码)编译,输出Typescript目标代码(也就是后面会讲的
NgFactories
)。这一步是Angular编译的核心,我们在后文仔细研究。后面将反复提及“AOT步骤2.1”。 - 2.2
ngc
调用tsc
将应用的Typescript代码编译成Javascript代码(包括2.1产生的、我们写的源代码、Angular框架的Typescript代码)。
为什么
ngc
不直接生成Javascript代码呢,因为经过tsc
的编译能发现Angular程序员的类型错误,比如class没有定义a属性你却去访问它。
哪些代码是需要编译的?根据tsconfig-aot.json的"files"字段,以app.module.ts
和main.ts
为起点,直接或间接import
的所有Typescript都需要编译。 - 2.1 将Angular源代码(此时是Typescript代码)编译,输出Typescript目标代码(也就是后面会讲的
-
摇树优化(Tree shaking),将没有用的代码删掉。
Angular文档:Tree shaking and AOT compilation are separate steps. Tree shaking can only target JavaScript code(目前的工具只能对Javascript代码进行摇树优化). AOT compilation converts more of the application to JavaScript,which in turn makes more of the application "tree shakable".
- 打包、混淆、压缩。
- 将得到的bundle以及其他需要的静态资源部署到服务器上。
以下是发生在客户端(用户浏览器)的步骤: - 客户端下载bundle,开始执行这些JavaScript。
- Angular启动,由于bundle中已经有了
NgFactories
的Javascript代码,因此Angular直接用它们来创建各种组件的实例,产生了我们看到的应用。
Angular编译(JIT步骤6、AOT步骤2.1)的顺序
Angular编译器输入NgModule,编译其中的entryComponents指定的那些组件。对每个entryComponents都产生对应的ComponentFactory类型,保存在一个ComponentFactoryResolver类型中。最后输出NgModuleFactory类型。
我们知道,组件的模板中可以引用别的组件,从而构成了组件树。entryComponents就是组件树的根节点,每一个entryComponents都引申出一颗组件树。编译器从一个entryComponent出发,就能编译到组件树中的所有组件。虽然编译器为每个组件都生成了工厂函数,但是只需要将entryComponents的工厂函数保存在ComponentFactoryResolver对象中就够了,因为父组件工厂在创建实例的时候会自动调用子组件的工厂。
为什么产生的都是类型而不是对象?因为编译是静态的,编译器只能依赖于静态的数据(编译器只是静态地提取分析decorators和Metadata;编译器不会执行源代码、也不知道我们定义的那些函数是干什么的),并且产生静态的结果(输出客户端要执行代码),只有类型这种静态的信息能够用代码来表示。而对象是动态的,可以将它理解为运行时在内存中的一段数据,它不能用ts/js代码来表示。
编译一个组件的时候,除了需要这个组件的模板和Metadata信息,编译器还需要知道此NgModule中声明的其他组件、指令、管道信息(因为在这个组件中可能会使用它们)。
在运行时,Angular会使用NgModuleFactory创建出模块的实例:NgModuleRef。
在NgModuleRef中有一个重要的属性:componentFactoryResolver,它就是刚才那个ComponentFactoryResolver类型的实例,给它一个组件类(类型在运行时的形态,即function),它会给你返回对应的ComponentFactory类型实例。
AOT步骤2.1产生的NgFactories
NgFactories
是浏览器真正执行的代码(如果是Typescript形式的,则需要先编译成Javascript)。每个组件、NgModule都会生成对应的工厂。当需要产生某个组件的实例的时候,Angular用组件工厂来创建组件、渲染组件——这涉及DOM操作、执行变化检测——获取oldValue和newValue并对比、销毁组件。NgModule
实例也是Angular用NgModule factory来创建的。
Angular文档:JIT compilation generates these same NgFactories in memory where they are largely invisible. AOT compilation reveals them as separate,physical files.
其实无论是AOT还是JIT,angular-complier都输出NgFactories
,只不过AOT产生的输出到*.ngfactory.ts
文件中,JIT产生的输出到客户端内存中。
Angular文档:Each component factory creates an instance of the component at runtime by combining the original class file and a JavaScript representation of the component's template. Note that the original component class is still referenced internally by the generated factory.
每一个component factory可以在运行时创建组件的实例,通过组合组件类(比如class AppComponent
)和组件模板的JavaScript表示。注意,在*.ngfactory.ts
中,仍然引用源文件中的组件类(见下例)。
这是步骤2.1产生的其中一个文件app.component.ngfactory.ts
:
/** * @fileoverview This file is generated by the Angular template compiler. * Do not edit. * @suppress {suspicIoUsCode,uselessCode,missingProperties,missingOverride} */ /* tslint:disable */ import * as i0 from './app.component.css.shim.ngstyle'; import * as i1 from '@angular/core'; import * as i2 from '../../../src/app/app.component'; import * as i3 from '@angular/common'; import * as i4 from '@angular/forms'; import * as i5 from './child1.component.ngfactory'; import * as i6 from '../../../src/app/child1.component'; const styles_AppComponent:any[] = [i0.styles]; export const RenderType_AppComponent:i1.RendererType2 = i1.ɵcrt({encapsulation:0,styles:styles_AppComponent,data:{}}); function View_AppComponent_1(_l:any):i1.ɵViewDefinition { return i1.ɵvid(0,[(_l()(),i1.ɵeld(0,(null as any),1,'h1',([] as any[]),(null as any))),(_l()(),i1.ɵted((null as any),['This is heading']))],(null as any)); } function View_AppComponent_2(_l:any):i1.ɵViewDefinition { return i1.ɵvid(0,'div',['','']))],(_ck,_v) => { const currVal_0:any = _v.context.$implicit; _ck(_v,currVal_0); }); } export function View_AppComponent_0(_l:any):i1.ɵViewDefinition { return i1.ɵvid(0,'button',[[(null as any),'click']],(_v,en,$event) => { var ad:boolean = true; var _co:i2.AppComponent = _v.component; if (('click' === en)) { const pd_0:any = ((<any>_co.toggleHeading()) !== false); ad = (pd_0 && ad); } return ad; },['Toggle Heading'])),['\n'])),i1.ɵand(16777216,View_AppComponent_1)),i1.ɵdid(16384,i3.NgIf,[i1.ViewContainerRef,i1.TemplateRef],{ngIf:[0,'ngIf']},(null as any)),['\n\n'])),'h3',['List of Heroes'])),View_AppComponent_2)),i1.ɵdid(802816,i3.NgForOf,i1.TemplateRef,i1.IterableDiffers],{ngForOf:[0,'ngForOf']},'h5',['my name: ',''])),5,'input',[['type','text']],[[2,'ng-untouched',(null as any)],[2,'ng-touched','ng-pristine','ng-dirty','ng-valid','ng-invalid','ng-pending',(null as any)]],'ngModelChange'],[(null as any),'input'],'blur'],'compositionstart'],'compositionend']],$event) => { var ad:boolean = true; var _co:i2.AppComponent = _v.component; if (('input' === en)) { const pd_0:any = ((<any>i1.ɵnov(_v,16)._handleInput($event.target.value)) !== false); ad = (pd_0 && ad); } if (('blur' === en)) { const pd_1:any = ((<any>i1.ɵnov(_v,16).onTouched()) !== false); ad = (pd_1 && ad); } if (('compositionstart' === en)) { const pd_2:any = ((<any>i1.ɵnov(_v,16)._compositionStart()) !== false); ad = (pd_2 && ad); } if (('compositionend' === en)) { const pd_3:any = ((<any>i1.ɵnov(_v,16)._compositionEnd($event.target.value)) !== false); ad = (pd_3 && ad); } if (('ngModelChange' === en)) { const pd_4:any = ((<any>(_co.myName = $event)) !== false); ad = (pd_4 && ad); } return ad; },i4.DefaultValueAccessor,[i1.Renderer2,i1.ElementRef,i4.COMPOSITION_BUFFER_MODE]],i1.ɵprd(1024,i4.NG_VALUE_ACCESSOR,(p0_0:any) => { return [p0_0]; },[i4.DefaultValueAccessor]),i1.ɵdid(671744,i4.NgModel,[[8,[8,i4.NG_VALUE_ACCESSOR]],{model:[0,'model']},{update:'ngModelChange'}),i1.ɵprd(2048,i4.NgControl,[i4.NgModel]),i4.NgControlStatus,[i4.NgControl],'child1',i5.View_Child1Component_0,i5.RenderType_Child1Component)),i1.ɵdid(49152,i6.Child1Component,{ipt:[0,'ipt']},['\n']))],_v) => { var _co:i2.AppComponent = _v.component; const currVal_0:any = _co.showHeading; _ck(_v,4,currVal_0); const currVal_1:any = _co.heroes; _ck(_v,10,currVal_1); const currVal_10:any = _co.myName; _ck(_v,18,currVal_10); const currVal_12:any = _co.myName; _ck(_v,26,currVal_12); },_v) => { var _co:i2.AppComponent = _v.component; const currVal_2:any = _co.myName; _ck(_v,13,currVal_2); const currVal_3:any = i1.ɵnov(_v,20).ngClassUntouched; const currVal_4:any = i1.ɵnov(_v,20).ngClassTouched; const currVal_5:any = i1.ɵnov(_v,20).ngClassPristine; const currVal_6:any = i1.ɵnov(_v,20).ngClassDirty; const currVal_7:any = i1.ɵnov(_v,20).ngClassValid; const currVal_8:any = i1.ɵnov(_v,20).ngClassInvalid; const currVal_9:any = i1.ɵnov(_v,20).ngClassPending; _ck(_v,15,currVal_3,currVal_4,currVal_5,currVal_6,currVal_7,currVal_8,currVal_9); const currVal_11:any = _co.someText; _ck(_v,23,currVal_11); }); } export function View_AppComponent_Host_0(_l:any):i1.ɵViewDefinition { return i1.ɵvid(0,'my-app',View_AppComponent_0,RenderType_AppComponent)),i2.AppComponent,(null as any))],(null as any)); } export const AppComponentNgFactory:i1.ComponentFactory<i2.AppComponent> = i1.ɵccf('my-app',View_AppComponent_Host_0,{},([] as any[]));
可以看出,在app.component.ngfactory.ts
中import
了我们写的app.component.ts
文件。更具体地说,是引用了其中的AppComponent类
来作为变量_co
的类型,你可以看看代码中的变量i2
在哪里被使用。
_co
是通过new AppComponent()
实例化的对象,是"context"的缩写,表示该组件的上下文。
-
"View_AppComponent_"+数字
- the internal component,负责(根据template)渲染出组件的视图,和进行变化检测。> 在这篇文章(以及多数前端相关的文章),渲染的意思是构建出DOM树,DOM是Javascript控制Web应用显示的接口。
-
"View_AppComponent_Host_"+数字
- the internal host component,负责渲染出宿主元素<my-app></my-app>
,并且使用"the internal component"管理组件的内部视图。 -
AppComponentNgFactory
- 类型是ComponentFactory<AppComponent>
。使用"the internal host component"来实例化组件(见 ComponentRef API)。
以下图片表示了*.component.ngfactory.ts
中各种对象之间的关系:
为什么在模板中只能访问public属性
如果在AppComponent
中定义属性private someText = 'hahaha';
然后在template中这样绑定{{someText}}
,那么在进行AOT编译的时候会报错(更具体地说,是步骤2.2),将private
去掉以后又可以成功进行AOT编译。
这是因为在app.component.ngfactory.ts
中,通过const currVal_11:any = _co.someText;
这样的方式访问context(上下文对象)的属性,所以如果someText
是AppComponent
的private属性,那么tsc在编译的时候就会报错。
如果通过JIT方式编译,在模板中访问private属性不会出现问题。前面说过JIT直接生成Javascript代码,不区分private和public。
如果你实在是既要在模板中访问某属性,又要将这个属性设置为private(处于封装性的考虑),你可以看看参考资料5的"AoT and encapsulation"章节。
AOT步骤2.1如何解析文件的Metadata
Angular编译器通过metadata中提供的信息,来生成组件/NgModule的工厂。
Angular编译器是如何解析文件的Metadata的呢?它怎么能从我们写的源代码中读懂代码的语义呢?
我们通过decorator(比如@Component(),@Input())来将Metadata附加到JavaScript类上。Metadata告诉Angular compiler如何处理这个Component/NgModule。在构造函数的声明中也包含了隐式的Metadata。
比如constructor(private heroService: HeroService){}
告诉编译器:该组件需要注入HeroService这个依赖。
即使Typescript被tsc编译成Javascript,Metadata依然保留着。这也是为什么JIT与AOT的原理是相同的。
AOT编译(AOT步骤2.1)分为两个阶段:
-
"AOT collector"收集每个源文件的Metadata,并为每个源文件输出一个
*.Metadata.json
文件,它是Metadata的abstract syntax tree (AST)表示,见下面的参考资料2。> "AOT collector"并不尝试去理解Metadata信息,它只是将其中的信息放进AST。
- "compiler"解析
*.Metadata.json
中的AST,生成Typescript代码。这里的"compiler"是更狭义的编译器,你可以将它理解为编译器的核心部分。
前面已经说过,生成的Typescript代码会引用我们写的源文件。为什么这是必须要的?因为"compiler"的输入仅仅是
*.Metadata.json
而已,它并不知道程序员写的业务逻辑(constructor中的代码、clickHandler中的代码、其他自定义函数中的代码),这些业务逻辑代码的执行依然要交给源文件中定义的组件类(比如AppComponent
)。
因此,Angular源代码要想通过编译,要先后满足:
- Metadata能被"AOT collector"识别并表示成AST。AOT collector只能识别一部分表达式语法,并且它不能识别箭头函数。如果违反了这两点,AOT collector将在AST的对应位置记录一个“错误节点”。如果稍后compiler要用到这个位置的节点,compiler会报错。
- AST节点能被compiler解析。compiler只能访问那些被export的symbol,因此未export的symbol不能作为AST的节点。此外,compiler只允许在Metadata中创建某些类的实例、只支持某些decorators、只能在Metadata中调用一小部分的函数,详见官方文档。
官方文档说:"Decorated component class members must be public. You cannot make an @Input() property private or internal."但是经过实验,
@Input() private ipt: any;
这样的代码不会出问题(只要不将私有的ipt
变量绑定在模板上)。官方文档还说:"Data bound properties must also be public"。这句话虽然是对的,但是它被放在了Phase 2: code generation这一节,这是有问题的。因为“在模板中绑定私有变量”的出错时间不是在AOT步骤2.1,而是步骤2.2。见下图:此时
app.component.ngfactory.ts
已经生成了,说明compiler已经解析AST完毕,只不过产生的代码违反了Typescript的私有成员访问限制,这才造成步骤2.2的错误。
尚未完成
https://www.youtube.com/watch...
2最快,3慢一点点,1明显最慢
Angular使用第3种方法,因为在创建DOM的同时可以拿到以后需要使用的Node(userNameText),而其他方法需要在DOM树中寻找(walk the path)。