這是我用來進行實驗的代碼,它是基於quickstart項目,並根據aot文檔修改獲得的。各位能夠用它來進行探索,也能夠本身基於quickstart進行修改(我的建議後者)。css
2018年2月17日更新:最近又作了2個小Demo用來研究Angular的編譯和打包,基於Angular5,一個使用rollup,一個使用webpack,(rollup目前沒法作到Angular的lazy loading)。不只項目文件結構很是簡潔,並且使用ngc(Angular compiler)的輸出做爲打包的輸入,這意味着:你不只能夠修改ts代碼而後查看ngc輸出有何變化,並且能夠修改ngc輸出而後查看最終的應用會如何運行,相似於玩「彙編」的感受,我相信這能加深學習者對Angular的理解甚至開啓源碼學習之路。前端
Angular應用由許多組件、指令、管道等組成,而且每一個組件有本身的HTML模板,它們按照Angular規定的語法進行組織。然而Angular的語法並不能被瀏覽器直接理解。爲了讓瀏覽器能運行咱們寫的項目,這些組件、指令、管道和HTML模板必須先被Angular編譯器編譯成瀏覽器可執行的Javascript。webpack
這個問題至關於:「爲何不讓用戶像之前同樣,寫瀏覽器能直接執行的JS代碼?」git
對於Angular來講,簡練的js代碼執行起來不高效(從時間、內存、文件大小的角度),高效的js代碼寫起來不簡練。爲了讓Angular既易於書寫又能擁有極高的效率,咱們能夠先用一種簡練的Angular語法表達咱們語義,而後讓編譯器根據咱們寫的源代碼編譯出同等語義的、真正用來執行的、但難以閱讀和手寫的js代碼。程序員
內存、文件大小的效率提高比較容易理解,Angular編譯器會輸出儘量高效(VM友好)、直接(犧牲可讀性)的代碼。時間上的效率提高很大程度來自於Angular2的變化檢測代碼對於Javascript虛擬機更友好,簡單來講就是爲 每一個組件都生成一段本身的變化檢測代碼,直接對這個組件的每個綁定逐一檢查,而不是像AngularJS同樣,對全部組件都同一個 通用的檢測算法。能夠閱讀 參考資料5的 Why we need compilation in Angular? 段落。
Android(或IOS) API
的視圖代碼(而不是使用DOM API的代碼)!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."普通的typescript項目須要用typescript編譯器(tsc)來編譯,而ngc是專用於Angular項目的tsc替代者。它內部封裝了tsc,還額外增長了用於Angular的選項、輸出額外的文件。
截圖自ng-conf視頻,除以上三種輸出以外ngc還能夠產生ngfactory
、ngstyle
文件。如視頻中所說,圖中三種輸出是Angular library(第三方庫,好比Angular Material)須要發佈的,ngfactory
、ngstyle
應該由library的使用者在編譯本身的Angular項目的時候產生(tsconfig
中的angularCompilerOptions.skipTemplateCodegen
字段能夠控制AOT是否產生這2種文件)。github
根據 最新的講座,在AOT模式下輸出的是ts代碼而不是js代碼。在JIT模式下直接輸出js代碼。tsc讀取tsconfig配置文件的
compilerOptions
部分,ngc讀取angularCompilerOptions
部分。webAngular文檔: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的區別只是編譯的時機和編譯所使用的工具庫。算法Angular文檔對.metadata.json的解釋。
.metadata.json
文件是Angular編譯器產生的,它用json的形式記錄了源.ts
中decorator信息、依賴注入信息,從而Angular二次編譯時再也不須要從.ts
中提取metadata(從而不須要.ts
源代碼的參與)。二次編譯的情形:第三方庫做者進行第一次編譯,產生圖中展現的三種文件併發布(不須要發佈.ts
源代碼),而後,庫的用戶將這些庫文件與本身的項目一塊兒編譯(第二次編譯),產生可運行的應用。若是你是Angular library的開發者而且但願你的library支持用戶進行AOT,那麼你須要發佈.metadata.json
和.js
文件,不然,你不須要.metadata.json
。
factory summaries(ngsummary.json
)已經包含了.metadata.json
中全部的信息,所以若是使用了ngsummary.json
,就不須要.metadata.json
了。typescript
JIT通常經歷的步驟:express
用tsc
將Typescript代碼(包括用戶代碼,以及Angular框架、Angular編譯器代碼)編譯成JavaScript代碼。
其中,metadata(也就是裝飾器中的對象,metadata中的信息是用戶提供的,好比template字符串)會被保存到類構造函數上。你能夠經過這個例子看出,typescript的decorator是如何被編譯的:http://www.typescriptlang.org/play/#src=%40Component(%7B%0A%20%20selector%3A%20'app'%2C%0A%20%20template%3A%20'%3Cp%3EHello%20%7B%7Bname%7D%7D!%3C%2Fp%3E'%0A%7D)%0Aclass%20MyComponent%20%7B%0A%20%20public%20name%20%3D%20'Angular'%3B%0A%7D
其中 Component這個函數是 Angular提供的,這個函數的功能就是將metadata 保存在類的構造函數上(MyComponent.__annotations__.push(metadata)
)。
metadata的做用就是向Angular編譯器提供信息。在後面,這些信息會被提取出來,給Angular編譯器使用。
Angular啓動,Angular調用Angular編譯器。對於每一個組件類(組件、ngModule、Pipe等都須要編譯,這篇文章咱們討論最重要的組件編譯),提取第2步保存的metadata,根據metadata編譯出瀏覽器能夠執行的Javascript代碼(也就是後面會講的NgFactories
)。
編譯組件能夠被簡單地理解爲:根據 Component metadata中的信息(尤爲是 template字符串),輸出建立對應DOM樹的js代碼(NgFactories
)。後面會詳細討論。
Angular應用的執行起點是main.js(由main.ts編譯獲得)。
NgFactories
),產生了咱們看到的應用。AOT通常經歷的步驟:
用ngc
編譯應用,其中包括兩步:
NgFactories
)。這一步是Angular編譯的核心,咱們在後文仔細研究。後面將反覆說起「AOT步驟2.1」。ngc
調用tsc
將應用的Typescript代碼編譯成Javascript代碼(包括2.1產生的、咱們寫的源代碼、Angular框架的Typescript代碼)。將ts編譯爲js的過程當中,能發現Angular程序中的類型錯誤,好比class沒有定義a屬性你卻去訪問它。
哪些代碼是須要編譯的?根據 tsconfig-aot.json的"files"字段,以app.module.ts
和main.ts
爲起點,直接或間接import
的全部.ts
都須要編譯。固然,Lazy loading module因爲沒有被import
而不會被加入bundle中,可是 Angular AOT Webpack 插件會智能地找到Lazy loading module並將它編譯成另一個bundle。
搖樹優化(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".
如下是發生在客戶端(用戶瀏覽器)的步驟:
NgFactories
的Javascript代碼,所以Angular直接用它們來建立各類組件的實例,產生了咱們看到的應用。
Angular編譯器輸入NgModule,編譯其中的entryComponents指定的那些組件。對每一個entryComponents都產生對應的ComponentFactory類型,保存在一個ComponentFactoryResolver類型中。最後輸出NgModuleFactory類型。
咱們知道,組件的模板中能夠引用別的組件,從而構成了 組件樹。entryComponents就是組件樹的 根節點,每個entryComponents都引伸出一顆組件樹。編譯器從一個entryComponent出發,就能編譯到組件樹中的全部組件。雖然編譯器爲 每一個組件都生成了工廠函數,可是隻須要將 entryComponents的工廠函數保存在ComponentFactoryResolver對象中就夠了,由於 父組件工廠在建立實例的時候會遞歸調用子組件的工廠。所以運行時只須要調用根組件的工廠函數,就能獲得一顆組件樹。爲何產生的都是類型而不是對象?由於編譯是靜態的,編譯器只能依賴於靜態的數據(編譯器只是靜態地提取分析decorators和metadata;編譯器不會執行源代碼、也不知道咱們定義的那些函數是幹什麼的),而且產生靜態的結果(輸出客戶端要執行代碼),只有類型這種靜態的信息可以用代碼來表示。而對象是動態的,它是運行時在內存中的一段數據,不能用ts/js代碼來表示。
NgModules是編譯組件的上下文:編譯一個組件的時候,除了須要本組件的模板和metadata信息,編譯器還須要知道當前NgModule中聲明的其餘組件、指令、管道,由於在這個組件的template中可能使用它們。因此,不像AngularJS,組件、指令、管道不是全局有效的,只有聲明(declare)了它們的NgModule,或者import它們所在的NgModule,才能使用它們,不然編譯報錯。這有助於在大型項目中隔離功能模塊、防止命名(selector)衝突。
在運行時,Angular會使用NgModuleFactory建立出模塊的實例:NgModuleRef。
在NgModuleRef中有一個重要的屬性:componentFactoryResolver,它就是剛纔那個ComponentFactoryResolver類型的實例,給它一個組件類(類型在運行時的形態,即function),它會給你返回對應的ComponentFactory類型實例。
NgFactories
是瀏覽器真正執行的代碼(若是是Typescript形式的,則須要先編譯成Javascript)。每一個組件、NgModule都會生成對應的工廠。組件工廠中包含了建立組件、渲染組件——這涉及DOM操做、執行變化檢測——獲取oldValue和newValue並對比、銷燬組件的邏輯。當須要產生某個組件的實例的時候,Angular用組件工廠來實例化一個組件對象。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能夠在運行時建立組件的實例,經過組合組件類(好比classAppComponent
)和組件模板的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),(null as any),1,'h1',([] as any[]), (null as any),(null as any),(null as any),(null as any),(null as any))),(_l()(), i1.ɵted((null as any),['This is heading']))],(null as any),(null as any)); } function View_AppComponent_2(_l:any):i1.ɵViewDefinition { return i1.ɵvid(0,[(_l()(),i1.ɵeld(0,(null as any),(null as any),1,'div',([] as any[]), (null as any),(null as any),(null as any),(null as any),(null as any))),(_l()(), i1.ɵted((null as any),['','']))],(null as any),(_ck,_v) => { const currVal_0:any = _v.context.$implicit; _ck(_v,1,0,currVal_0); }); } export function View_AppComponent_0(_l:any):i1.ɵViewDefinition { return i1.ɵvid(0,[(_l()(),i1.ɵeld(0,(null as any),(null as any),1,'button',([] as any[]), (null as any),[[(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; },(null as any),(null as any))),(_l()(),i1.ɵted((null as any),['Toggle Heading'])), (_l()(),i1.ɵted((null as any),['\n'])),(_l()(),i1.ɵand(16777216,(null as any), (null as any),1,(null as any),View_AppComponent_1)),i1.ɵdid(16384,(null as any), 0,i3.NgIf,[i1.ViewContainerRef,i1.TemplateRef],{ngIf:[0,'ngIf']},(null as any)), (_l()(),i1.ɵted((null as any),['\n\n'])),(_l()(),i1.ɵeld(0,(null as any),(null as any), 1,'h3',([] as any[]),(null as any),(null as any),(null as any),(null as any), (null as any))),(_l()(),i1.ɵted((null as any),['List of Heroes'])),(_l()(), i1.ɵted((null as any),['\n'])),(_l()(),i1.ɵand(16777216,(null as any),(null as any), 1,(null as any),View_AppComponent_2)),i1.ɵdid(802816,(null as any),0,i3.NgForOf, [i1.ViewContainerRef,i1.TemplateRef,i1.IterableDiffers],{ngForOf:[0,'ngForOf']}, (null as any)),(_l()(),i1.ɵted((null as any),['\n\n'])),(_l()(),i1.ɵeld(0, (null as any),(null as any),1,'h5',([] as any[]),(null as any),(null as any), (null as any),(null as any),(null as any))),(_l()(),i1.ɵted((null as any), ['my name: ',''])),(_l()(),i1.ɵted((null as any),['\n'])),(_l()(),i1.ɵeld(0, (null as any),(null as any),5,'input',[['type','text']],[[2,'ng-untouched', (null as any)],[2,'ng-touched',(null as any)],[2,'ng-pristine',(null as any)], [2,'ng-dirty',(null as any)],[2,'ng-valid',(null as any)],[2,'ng-invalid', (null as any)],[2,'ng-pending',(null as any)]],[[(null as any),'ngModelChange'], [(null as any),'input'],[(null as any),'blur'],[(null as any),'compositionstart'], [(null as any),'compositionend']],(_v,en,$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; },(null as any),(null as any))),i1.ɵdid(16384,(null as any),0,i4.DefaultValueAccessor, [i1.Renderer2,i1.ElementRef,[2,i4.COMPOSITION_BUFFER_MODE]],(null as any), (null as any)),i1.ɵprd(1024,(null as any),i4.NG_VALUE_ACCESSOR,(p0_0:any) => { return [p0_0]; },[i4.DefaultValueAccessor]),i1.ɵdid(671744,(null as any),0,i4.NgModel,[[8,(null as any)], [8,(null as any)],[8,(null as any)],[2,i4.NG_VALUE_ACCESSOR]],{model:[0, 'model']},{update:'ngModelChange'}),i1.ɵprd(2048,(null as any),i4.NgControl, (null as any),[i4.NgModel]),i1.ɵdid(16384,(null as any),0,i4.NgControlStatus, [i4.NgControl],(null as any),(null as any)),(_l()(),i1.ɵted((null as any), ['\n\n'])),(_l()(),i1.ɵeld(0,(null as any),(null as any),1,'h5',([] as any[]), (null as any),(null as any),(null as any),(null as any),(null as any))), (_l()(),i1.ɵted((null as any),['',''])),(_l()(),i1.ɵted((null as any),['\n\n'])), (_l()(),i1.ɵeld(0,(null as any),(null as any),1,'child1',([] as any[]),(null as any), (null as any),(null as any),i5.View_Child1Component_0,i5.RenderType_Child1Component)), i1.ɵdid(49152,(null as any),0,i6.Child1Component,([] as any[]),{ipt:[0,'ipt']}, (null as any)),(_l()(),i1.ɵted((null as any),['\n']))],(_ck,_v) => { var _co:i2.AppComponent = _v.component; const currVal_0:any = _co.showHeading; _ck(_v,4,0,currVal_0); const currVal_1:any = _co.heroes; _ck(_v,10,0,currVal_1); const currVal_10:any = _co.myName; _ck(_v,18,0,currVal_10); const currVal_12:any = _co.myName; _ck(_v,26,0,currVal_12); },(_ck,_v) => { var _co:i2.AppComponent = _v.component; const currVal_2:any = _co.myName; _ck(_v,13,0,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,0,currVal_3,currVal_4,currVal_5,currVal_6,currVal_7,currVal_8,currVal_9); const currVal_11:any = _co.someText; _ck(_v,23,0,currVal_11); }); } export function View_AppComponent_Host_0(_l:any):i1.ɵViewDefinition { return i1.ɵvid(0,[(_l()(),i1.ɵeld(0,(null as any),(null as any),1,'my-app',([] as any[]), (null as any),(null as any),(null as any),View_AppComponent_0,RenderType_AppComponent)), i1.ɵdid(49152,(null as any),0,i2.AppComponent,([] as any[]),(null as any),(null as any))], (null as any),(null as any)); } export const AppComponentNgFactory:i1.ComponentFactory<i2.AppComponent> = i1.ɵccf('my-app', i2.AppComponent,View_AppComponent_Host_0,{},{},([] as any[]));
變量名是否是很奇怪?這是爲了防止命名衝突,因此在export的時候增長了一些特殊的字符,這些名字表明什麼能夠在 codegen_private_exports.ts或 identifiers.ts中找到。
能夠看出,在app.component.ngfactory.ts
中import
了咱們寫的app.component.ts
文件。更具體地說,是引用了其中的AppComponent類
來做爲變量_co
的類型,你能夠看看代碼中的變量i2
在哪裏被使用。
_co
是"context"的縮寫。context(上下文)是 組件類在運行時實例化的對象(好比經過new AppComponent()
)。組件類徹底是由Angular開發者編寫的,Angular用context中的數據來渲染template(建立view)、更新view。
"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
中各類對象之間的關係:
若是在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"章節。
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。
*.metadata.json
中的AST,生成Typescript代碼。這裏的"compiler"是更狹義的編譯器,你能夠將它理解爲編譯器的核心部分。前面已經說過,生成的Typescript代碼會引用咱們寫的源文件。爲何這是必需要的?由於"compiler"的輸入僅僅是*.metadata.json
而已,它並不知道程序員寫的業務邏輯(constructor中的代碼、clickHandler中的代碼、其餘自定義函數中的代碼),這些業務邏輯代碼的執行依然要交給源文件中定義的組件類(好比AppComponent
)。
所以,Angular源代碼要想經過編譯,要前後知足:
官方文檔說:"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...
HTML、表達式綁定、指令
如何建立DOM元素:
2最快,3慢一點點,1明顯最慢
Angular使用第3種方法,由於在建立DOM的同時能夠拿到之後須要使用的Node(userNameText),而其餘方法須要在DOM樹中尋找(walk the path)。
如何建立指令實例:ngElement
如何創建綁定:Binding類
「View」是template的實例,同一個template能夠屢次重複使用。每次使用就將數據填充到template中,產生view。
ngElement數據結構優化:將指令Map改成Inline
優化:View cache