Angular編譯機制
前言
這是我用來進行實驗的代碼,它是基于quickstart項目,并根據aot文檔修改得到的。各位可以用它來進行探索,也可以自己基于quickstart進行修改(個人建議後者)。
2018年2月17日更新:最近又做了2個小Demo用來研究Angular的編譯和打包,基于Angular5,一個使用rollup,一個使用webpack,(rollup目前無法做到Angular的lazy loading)。不僅項目檔案結構非常簡潔,而且使用ngc(Angular compiler)的輸出作為打包的輸入,這意味着:你不僅可以修改ts代碼然後檢視ngc輸出有何變化,而且可以修改ngc輸出然後檢視最終的應用會如何運作,類似于玩“彙編”的感覺,我相信這能加深學習者對Angular的了解甚至開啟源碼學習之路。
什麼是Angular編譯
Angular應用由許多元件、指令、管道等組成,并且每個元件有自己的HTML模闆,它們按照Angular規定的文法進行組織。然而Angular的文法并不能被浏覽器直接了解。為了讓浏覽器能運作我們寫的項目,這些元件、指令、管道和HTML模闆必須先被Angular編譯器編譯成浏覽器可執行的Javascript。
為什麼Angular需要編譯
這個問題相當于:“為什麼不讓使用者像以前一樣,寫浏覽器能直接執行的JS代碼?”
對于Angular來說,簡練的js代碼執行起來不高效(從時間、記憶體、檔案大小的角度),高效的js代碼寫起來不簡練。為了讓Angular既易于書寫又能擁有極高的效率,我們可以先用一種簡練的Angular文法表達我們語義,然後讓編譯器根據我們寫的源代碼編譯出同等語義的、真正用來執行的、但難以閱讀和手寫的js代碼。
記憶體、檔案大小的效率提升比較容易了解,Angular編譯器會輸出盡可能高效(VM友好)、直接(犧牲可讀性)的代碼。時間上的效率提升很大程度來自于Angular2的變化檢測代碼對于Javascript虛拟機更友好,簡單來說就是為每個元件都生成一段自己的變化檢測代碼,直接對這個元件的每一個綁定逐一檢查,而不是像AngularJS一樣,對所有元件都同一個通用的檢測算法。可以閱讀參考資料5的 Why we need compilation in Angular? 段落。
編譯可以讓Angular與用戶端(浏覽器)解耦,因為Angular模闆文法平台無關,抽象程度更高。這意味着,可以用另一種編譯器,輸入相同的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."
Angular編譯器(ngc)
普通的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種檔案)。
根據最新的講座,在AOT模式下輸出的是ts代碼而不是js代碼。在JIT模式下直接輸出js代碼。
tsc讀取tsconfig配置檔案的compilerOptions部分,ngc讀取angularCompilerOptions部分。
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的差別隻是編譯的時機和編譯所使用的工具庫。
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了。
just-in-time (JIT)
JIT一般經曆的步驟:
使用者使用Typescript和Angular模闆文法編寫源代碼。
用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編譯器使用。
打包、混淆、壓縮。
将得到的bundle以及其他需要的靜态資源部署到伺服器上。
以下是發生在用戶端(使用者浏覽器)的步驟:
用戶端下載下傳bundle,開始執行這些JavaScript。
Angular啟動,Angular調用Angular編譯器。對于每個元件類(元件、ngModule、Pipe等都需要編譯,這篇文章我們讨論最重要的元件編譯),提取第2步儲存的metadata,根據metadata編譯出浏覽器可以執行的Javascript代碼(也就是後面會講的NgFactories)。
編譯元件可以被簡單地了解為:根據Component metadata中的資訊(尤其是template字元串),輸出建立對應DOM樹的js代碼(NgFactories)。後面會詳細讨論。
Angular應用的執行起點是main.js(由main.ts編譯得到)。
建立各種元件的執行個體(通過NgFactories),産生了我們看到的應用。
Ahead-of-time (AOT)
AOT一般經曆的步驟:
程式員用Typescript和Angular文法編寫源代碼。
用ngc編譯應用,其中包括兩步:
2.1 将Angular源代碼(此時是Typescript代碼)編譯,輸出Typescript目标代碼(也就是後面會講的NgFactories)。這一步是Angular編譯的核心,我們在後文仔細研究。後面将反複提及“AOT步驟2.1”。
2.2 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".
打包、混淆、壓縮。
将得到的bundle以及其他需要的靜态資源部署到伺服器上。
以下是發生在用戶端(使用者浏覽器)的步驟:
用戶端下載下傳bundle,開始執行這些JavaScript。
Angular啟動,由于bundle中已經有了NgFactories的Javascript代碼,是以Angular直接用它們來建立各種元件的執行個體,産生了我們看到的應用。
Angular編譯(JIT步驟6、AOT步驟2.1)的順序
我們知道,元件的模闆中可以引用别的元件,進而構成了元件樹。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類型執行個體。
AOT步驟2.1産生的NgFactories
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可以在運作時建立元件的執行個體,通過組合元件類(比如class AppComponent)群組件模闆的JavaScript表示。注意,在*.ngfactory.ts中,仍然引用源檔案中的元件類(見下例)。
這是步驟2.1産生的其中一個檔案app.component.ngfactory.ts:
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 = ((_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 = ((i1.ɵnov(_v,16)._handleInput($event.target.value)) !== false);
ad = (pd_0 && ad);
}
if (('blur' === en)) {
const pd_1:any = ((i1.ɵnov(_v,16).onTouched()) !== false);
ad = (pd_1 && ad);
}
if (('compositionstart' === en)) {
const pd_2:any = ((i1.ɵnov(_v,16)._compositionStart()) !== false);
ad = (pd_2 && ad);
}
if (('compositionend' === en)) {
const pd_3:any = ((i1.ɵnov(_v,16)._compositionEnd($event.target.value)) !== false);
ad = (pd_3 && ad);
}
if (('ngModelChange' === en)) {
const pd_4: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 = 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,負責渲染出宿主元素,并且使用"the internal component"管理元件的内部視圖。
AppComponentNgFactory - 類型是ComponentFactory。使用"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編譯器通過
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的錯誤。
尚未完成
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
參考資料