標籤:

如何了解Angular(非Angular.js)的實現原理?

使用了三年的Angular.js。剛開始不能理解Angular.js的思想,直到看了一本Angular.js權威指南,之後看了Build your own angular.js後豁然開朗。現在已經遷移到了Angular半年,將之前的Angular.js整個工程重構到Angular上,也在工作中填了不少坑,現在希望了解它的實現,需要去閱讀哪些文檔?或者從哪裡入手閱讀源碼?十分感謝!


Angular 的原理其實並不複雜,絕大多數源碼都是正統的 OO,在職能劃分和狀態轉移上都很清晰;此外相當一部分功能都直接通過編譯方式實現,與運行時相分離。

想要充分了解 Angular 的實現原理的話,不要先讀 Angular 的源碼,而是要先讀你的應用。

Angular 的應用可以分為三部分組成:

  • Angular 提供的運行時代碼;
  • Angular 編譯器編譯出的代碼;
  • 用戶提供的業務代碼。

顯然,對於「用戶代碼」我們是非常熟悉的,幾乎就是幾個固定的 Boilerplate,配合編輯器的 Snippet 或 CLI 工具很容易實現快速開發。

但往往我們最不熟悉的,並且也是對「實現原理」暴露最多的地方,就是「Angular 編譯器生成的代碼」了。只要完全理解這部分代碼,基本上也就對 Angular 有了一半以上的了解。

這裡以 Angular 2.x 為例,Angular 4+ 由於使用了 View Engine 進行重構(【MMR-A】全新的 View Engine 模式),對外暴露的細節反而減少了。

yarn global add @angular/cli@1.0.0-rc.4
ng new angular-explore
cd angular-explore

我們可以使用 Angular CLI 的 RC 版本來快速構造一個 Angular 2.x 的應用(Wish you have a good network),不過,我們今天的主角並不是 @angular/cli,而是 @angular/compiler-cli。

./node_module/.bin/ngc -p src/tsconfig.app.json

用 ng build -aot 也可以得到部分內容,不過會被構建到同一個 Bundle 中,不方便觀察。

這樣,神奇的事情就發生了,我們不小心多出來了不少文件,如圖所示:

我們可以見到(大致)這麼幾類文件,對於 Component:

  • &.component.ts
  • &.component.js
  • &.component.html
  • &.component.ngfactory.ts
  • &.component.css
  • &.component.css.shim.ngstyle.ts
  • &.component.ngsummary.json
  • &.component.metadata.json

對於 Module:

  • &.module.ts
  • &.module.js
  • &.module.ngfactory.ts
  • &.module.ngsummary.json
  • &.module.metadata.json

我們可以看出,ngc(Compiler CLI)主要做了兩點微小的工作:

  1. 將部分靜態資源編譯為 TypeScript;(在原目錄中,可通過 angularCompilerOptions.genDir 選項配置)
  2. 將部分 TypeScript 編譯為 JavaScript。(在 out-tsc 目錄中,可通過 outDir 選項配置)

如果我們打開 app.component.ngfactory.ts,我們可以看到很長的內容,如下:

/**
* @fileoverview This file is generated by the Angular 2 template compiler.
* Do not edit.
* @suppress {suspiciousCode,uselessCode,missingProperties}
*/
/* tslint:disable */

import * as import0 from "./app.component";
import * as import1 from "@angular/core/src/linker/view";
import * as import2 from "@angular/core/src/render/api";
import * as import3 from "@angular/core/src/linker/view_utils";
import * as import4 from "@angular/core/src/metadata/view";
import * as import5 from "@angular/core/src/linker/view_type";
import * as import6 from "@angular/core/src/change_detection/constants";
import * as import7 from "@angular/core/src/linker/component_factory";
import * as import8 from "./app.component.css.shim.ngstyle";
import * as import9 from "@angular/core/src/change_detection/change_detection_util";
export class Wrapper_AppComponent {
/*private*/ _eventHandler:Function;
context:import0.AppComponent;
/*private*/ _changed:boolean;
constructor() {
this._changed = false;
this.context = new import0.AppComponent();
}
ngOnDetach(view:import1.AppView&,componentView:import1.AppView&,el:any):void {
}
ngOnDestroy():void {
}
ngDoCheck(view:import1.AppView&,el:any,throwOnChange:boolean):boolean {
var changed:any = this._changed;
this._changed = false;
return changed;
}
checkHost(view:import1.AppView&,componentView:import1.AppView&,el:any,throwOnChange:boolean):void {
}
handleEvent(eventName:string,$event:any):boolean {
var result:boolean = true;
return result;
}
subscribe(view:import1.AppView&,_eventHandler:any):void {
this._eventHandler = _eventHandler;
}
}
var renderType_AppComponent_Host:import2.RenderComponentType = import3.createRenderComponentType("",0,import4.ViewEncapsulation.None,([] as any[]),{});
class View_AppComponent_Host0 extends import1.AppView& {
_el_0:any;
compView_0:import1.AppView&;
_AppComponent_0_3:Wrapper_AppComponent;
constructor(viewUtils:import3.ViewUtils,parentView:import1.AppView&,parentIndex:number,parentElement:any) {
super(View_AppComponent_Host0,renderType_AppComponent_Host,import5.ViewType.HOST,viewUtils,parentView,parentIndex,parentElement,import6.ChangeDetectorStatus.CheckAlways);
}
createInternal(rootSelector:string):import7.ComponentRef& {
this._el_0 = import3.selectOrCreateRenderHostElement(this.renderer,"app-root",import3.EMPTY_INLINE_ARRAY,rootSelector,(null as any));
this.compView_0 = new View_AppComponent0(this.viewUtils,this,0,this._el_0);
this._AppComponent_0_3 = new Wrapper_AppComponent();
this.compView_0.create(this._AppComponent_0_3.context);
this.init(this._el_0,((&this.renderer).directRenderer? (null as any): [this._el_0]),(null as any));
return new import7.ComponentRef_&(0,this,this._el_0,this._AppComponent_0_3.context);
}
injectorGetInternal(token:any,requestNodeIndex:number,notFoundResult:any):any {
if (((token === import0.AppComponent) (0 === requestNodeIndex))) { return this._AppComponent_0_3.context; }
return notFoundResult;
}
detectChangesInternal(throwOnChange:boolean):void {
this._AppComponent_0_3.ngDoCheck(this,this._el_0,throwOnChange);
this.compView_0.internalDetectChanges(throwOnChange);
}
destroyInternal():void {
this.compView_0.destroy();
}
visitRootNodesInternal(cb:any,ctx:any):void {
cb(this._el_0,ctx);
}
}
export const AppComponentNgFactory:import7.ComponentFactory& = new import7.ComponentFactory&("app-root",View_AppComponent_Host0,import0.AppComponent);
const styles_AppComponent:any[] = [import8.styles];
var renderType_AppComponent:import2.RenderComponentType = import3.createRenderComponentType("",0,import4.ViewEncapsulation.Emulated,styles_AppComponent,{});
export class View_AppComponent0 extends import1.AppView& {
_el_0:any;
_text_1:any;
_text_2:any;
/*private*/ _expr_3:any;
constructor(viewUtils:import3.ViewUtils,parentView:import1.AppView&,parentIndex:number,parentElement:any) {
super(View_AppComponent0,renderType_AppComponent,import5.ViewType.COMPONENT,viewUtils,parentView,parentIndex,parentElement,import6.ChangeDetectorStatus.CheckAlways);
this._expr_3 = import9.UNINITIALIZED;
}
createInternal(rootSelector:string):import7.ComponentRef& {
const parentRenderNode:any = this.renderer.createViewRoot(this.parentElement);
this._el_0 = import3.createRenderElement(this.renderer,parentRenderNode,"h1",import3.EMPTY_INLINE_ARRAY,(null as any));
this._text_1 = this.renderer.createText(this._el_0,"",(null as any));
this._text_2 = this.renderer.createText(parentRenderNode,"
",(null as any));
this.init((null as any),((&this.renderer).directRenderer? (null as any): [
this._el_0,
this._text_1,
this._text_2
]
),(null as any));
return (null as any);
}
detectChangesInternal(throwOnChange:boolean):void {
const currVal_3:any = import3.inlineInterpolate(1,"
",this.context.title,"
");
if (import3.checkBinding(throwOnChange,this._expr_3,currVal_3)) {
this.renderer.setText(this._text_1,currVal_3);
this._expr_3 = currVal_3;
}
}
}

雖然看上去內容很多,但實際上並不複雜。這個文件裡面有 3 個類,分別是:

  • Wrapper_AppComponent
  • View_AppComponent_Host0
  • View_AppComponent0

如果這三個類分別放在三個文件里,那讀起來可能很輕鬆。

對於 Wrapper_AppComponent,這裡也就是對我們的組件類進行了包裝。不過這樣看上去什麼事都沒有做不是么?是的,不過這是因為我們的組件太簡單了,如果我們給組件增加一些「假的」內容讓它看起來複雜些:

export class AppComponent implements OnInit, OnChanges, OnDestroy, DoCheck, AfterViewInit, AfterContentInit, AfterViewChecked, AfterContentChecked {
title = "app works!";

constructor(location: Location, vcRef: ViewContainerRef) { }

@HostBinding("class.abc")
hasAbcClass = true;

@HostListener("click")
onClick() { }

@HostListener("keydown")
onKeydown() { }

ngOnInit(): void { }
ngOnChanges(changes: SimpleChanges): void { }
ngOnDestroy(): void { }
ngDoCheck(): void { }
ngAfterViewInit(): void { }
ngAfterContentInit(): void { }
ngAfterViewChecked(): void { }
ngAfterContentChecked(): void { }
}

然後再次構建,之後的 Wrapper_AppComponent 就會變成:

export class Wrapper_AppComponent {
/*private*/ _eventHandler:Function;
context:import0.AppComponent;
/*private*/ _changed:boolean;
/*private*/ _changes:{[key: string]:any};
/*private*/ _expr_0:any;
constructor(p0:any,p1:any) {
this._changed = false;
this._changes = {};
this.context = new import0.AppComponent(p0,p1);
this._expr_0 = import1.UNINITIALIZED;
}
ngOnDetach(view:import2.AppView&,componentView:import2.AppView&,el:any):void {
}
ngOnDestroy():void {
this.context.ngOnDestroy();
}
ngDoCheck(view:import2.AppView&,el:any,throwOnChange:boolean):boolean {
var changed:any = this._changed;
this._changed = false;
if (!throwOnChange) {
if (changed) {
this.context.ngOnChanges(this._changes);
this._changes = {};
}
if ((view.numberOfChecks === 0)) { this.context.ngOnInit(); }
this.context.ngDoCheck();
}
return changed;
}
checkHost(view:import2.AppView&,componentView:import2.AppView&,el:any,throwOnChange:boolean):void {
const currVal_0:any = this.context.hasAbcClass;
if (import3.checkBinding(throwOnChange,this._expr_0,currVal_0)) {
view.renderer.setElementClass(el,"abc",currVal_0);
this._expr_0 = currVal_0;
}
}
handleEvent(eventName:string,$event:any):boolean {
var result:boolean = true;
if ((eventName == "click")) {
const pd_sub_0:any = ((&this.context.onClick()) !== false);
result = (pd_sub_0 result);
}
if ((eventName == "keydown")) {
const pd_sub_1:any = ((&this.context.onKeydown()) !== false);
result = (pd_sub_1 result);
}
return result;
}
subscribe(view:import2.AppView&,_eventHandler:any):void {
this._eventHandler = _eventHandler;
}
}

豐富了不少,這裡就可以看去來到底是怎麼一回事了。

constructor 中,我們的 Component 通過 new 運算符直接初始化,依賴注入的內容也是直接通過參數傳入的,而這些參數同時也被作為 Wrapper_AppComponent 的參數。我們的組件實例作為 context(也就是模版所使用的 ViewModel)。

ngOnDestroy 中,調用了我們組件的 ngOnDestroy。

ngDoCheck 中,如果有變化,就調用組件的 ngOnChanges;如果是第一次變化,就額外調用組件 ngOnInit;最後,還調用組件的 ngDoCheck。

checkHost 中,檢查我們的 hasAbcClass 綁定的值,如果有變化,就更新 "abc" 這個 class 的有無。

handleEvent 中,如果事件是 "click",就執行 onClick 回調,如果是 "keydown",就執行 onKeydown 這個回調。

是不是一目了然?現在,我們已經知道了 OnInit、OnChanges、OnDestroy、DoCheck 這 4 個生命周期的調用時機,以及宿主級屬性綁定(臟檢測)和宿主級事件綁定的實現方式。

之後便是兩個 AppView 的實現類。其中,View_AppComponent_Host0 稱為 HostView,適用於 entryComponent,即作為從與非 Angular 環境(應用邊界)毗鄰的根組件;而 View_AppComponent0 稱為 EmbeddedView,適用於嵌套在其它 Angular 組件模版中的子組件。兩者雖然都是從模版到視圖實例的過程,但實現方式有些許差異。

然後我們來看看 View_AppComponent_Host0 類,為了便於觀察,我們給 AppComponent 增加幾個 provider:

@Injectable()
export class TestClass {
constructor(location: Location) { }
}

export function TestFactory(location: Location) { }

@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"],
providers: [
{ provide: "MY_TOKEN", useValue: 12345 },
{ provide: TestClass, useClass: TestClass },
{ provide: TestFactory, useFactory: TestFactory, deps: [Location] }
]
})
export class AppComponent {}

然後得到 View_AppComponent_Host0 如下:

class View_AppComponent_Host0 extends import2.AppView& {
_el_0:any;
/*private*/ _vc_0:import6.ViewContainer;
compView_0:import2.AppView&;
_AppComponent_0_5:Wrapper_AppComponent;
__MY_TOKEN_0_6:any;
__TestClass_0_7:import0.TestClass;
__TestFactory_0_8:any;
_el_1:any;
constructor(viewUtils:import3.ViewUtils,parentView:import2.AppView&,parentIndex:number,parentElement:any) {
super(View_AppComponent_Host0,renderType_AppComponent_Host,import7.ViewType.HOST,viewUtils,parentView,parentIndex,parentElement,import8.ChangeDetectorStatus.CheckAlways);
}
get _MY_TOKEN_0_6():any {
if ((this.__MY_TOKEN_0_6 == null)) { (this.__MY_TOKEN_0_6 = 12345); }
return this.__MY_TOKEN_0_6;
}
get _TestClass_0_7():import0.TestClass {
if ((this.__TestClass_0_7 == null)) { (this.__TestClass_0_7 = new import0.TestClass(this.injectorGet(import9.Location,this.parentIndex))); }
return this.__TestClass_0_7;
}
get _TestFactory_0_8():any {
if ((this.__TestFactory_0_8 == null)) { (this.__TestFactory_0_8 = import0.TestFactory(this.injectorGet(import9.Location,this.parentIndex))); }
return this.__TestFactory_0_8;
}
createInternal(rootSelector:string):import10.ComponentRef& {
this._el_0 = import3.selectOrCreateRenderHostElement(this.renderer,"app-root",import3.EMPTY_INLINE_ARRAY,rootSelector,(null as any));
this._vc_0 = new import6.ViewContainer(0,(null as any),this,this._el_0);
this.compView_0 = new View_AppComponent0(this.viewUtils,this,0,this._el_0);
this._AppComponent_0_5 = new Wrapper_AppComponent(this.injectorGet(import9.Location,this.parentIndex),this._vc_0.vcRef);
this.compView_0.create(this._AppComponent_0_5.context);
this._el_1 = this.renderer.createTemplateAnchor((null as any),(null as any));
var disposable_0:Function = import3.subscribeToRenderElement(this,this._el_0,new import3.InlineArray4(4,"click",(null as any),"keydown",(null as any)),this.eventHandler(this.handleEvent_0));
this.init(this._el_1,((&this.renderer).directRenderer? (null as any): [this._el_0]),[disposable_0]);
return new import10.ComponentRef_&(0,this,this._el_0,this._AppComponent_0_5.context);
}
injectorGetInternal(token:any,requestNodeIndex:number,notFoundResult:any):any {
if (((token === import0.AppComponent) (0 === requestNodeIndex))) { return this._AppComponent_0_5.context; }
if (((token === "MY_TOKEN") (0 === requestNodeIndex))) { return this._MY_TOKEN_0_6; }
if (((token === import0.TestClass) (0 === requestNodeIndex))) { return this._TestClass_0_7; }
if (((token === import0.TestFactory) (0 === requestNodeIndex))) { return this._TestFactory_0_8; }
return notFoundResult;
}
detectChangesInternal(throwOnChange:boolean):void {
this._AppComponent_0_5.ngDoCheck(this,this._el_0,throwOnChange);
this._vc_0.detectChangesInNestedViews(throwOnChange);
if (!throwOnChange) {
if ((this.numberOfChecks === 0)) { this._AppComponent_0_5.context.ngAfterContentInit(); }
this._AppComponent_0_5.context.ngAfterContentChecked();
}
this._AppComponent_0_5.checkHost(this,this.compView_0,this._el_0,throwOnChange);
this.compView_0.internalDetectChanges(throwOnChange);
if (!throwOnChange) {
if ((this.numberOfChecks === 0)) { this._AppComponent_0_5.context.ngAfterViewInit(); }
this._AppComponent_0_5.context.ngAfterViewChecked();
}
}
destroyInternal():void {
this._vc_0.destroyNestedViews();
this.compView_0.destroy();
this._AppComponent_0_5.ngOnDestroy();
}
visitRootNodesInternal(cb:any,ctx:any):void {
cb(this._vc_0.nativeElement,ctx);
this._vc_0.visitNestedViewRootNodes(cb,ctx);
cb(this._el_1,ctx);
}
handleEvent_0(eventName:string,$event:any):boolean {
this.compView_0.markPathToRootAsCheckOnce();
var result:boolean = true;
result = (this._AppComponent_0_5.handleEvent(eventName,$event) result);
return result;
}
}

作為 HostView,也就是要和應用外部(非 Angular 環境)打交道的視圖部分。

在 getter 部分,可以看到所有依賴的實例化方式都是靜態編譯出來的:如果是 useValue,就直接賦值;如果是 useClass,就通過 new 調用;如果是 useFactory,就通過普通函數調用。

在 createInternal 中,進行了一次選擇器查找(相當於調 querySelector)獲取根元素;實例化了 EmbeddedView;實例化了上面說到的 Wrapper_AppComponent 包裝類,其中依賴注入的內容由自身的 injectorGet 獲取;將 View 與 Context 綁定;監聽了 click 和 keydown 事件。

在 injectorGetInternal 中,定義了查詢過程,如果 Token 是我們在組件里定義的某個 provider,那就返回對應的屬性,如果是第一次獲取會使用上面說到的 getter 中的初始化方式,否則通過緩存的屬性返回。

在 detectChangesInternal 中,首先調用了 Wrapper 的 DoCheck;然後對所有嵌套視圖進行變化檢查,如果是首次檢查則出發 Component 的 ngAfterContentInit 方法,然後不論是否首次都觸發 ngAfterContentChecked 方法;檢查 Host 上的綁定和 EmbeddedView 的綁定,如果是第一次檢查完成則觸發 Component 的 ngAfterViewInit 方法,不論是否首次都觸發 ngAfterViewChecked 的方法。

在 destroyInternal 中,調用 EmbeddedView 和 Wrapper 的 destroy 相關方法。

於是,現在我們又知道了 AfterContentInit、AfterContentChecked、AfterViewInit、AfterViewChecked 這四個生命周期的調用時機,依賴注入的實現方式

接下來我們再看 View_AppComponent0,為了更好的理解內容,我們把模版擴展為:

&{{ title | uppercase }}& &

    &

  • Line {{ i }}& & &

    重新編譯後,得到 View_AppComponent0 的內容(對應於組件自身的整體模版):

    export class View_AppComponent0 extends import2.AppView& {
    _el_0:any;
    _text_1:any;
    _text_2:any;
    _el_3:any;
    _text_4:any;
    _anchor_5:any;
    /*private*/ _vc_5:import6.ViewContainer;
    _TemplateRef_5_5:any;
    _NgFor_5_6:import12.Wrapper_NgFor;
    _text_6:any;
    _text_7:any;
    _el_8:any;
    _text_9:any;
    /*private*/ _expr_13:any;
    _pipe_uppercase_0:import13.UpperCasePipe;
    _pipe_uppercase_0_0:any;
    _arr_16:any;
    /*private*/ _expr_17:any;
    constructor(viewUtils:import3.ViewUtils,parentView:import2.AppView&,parentIndex:number,parentElement:any) {
    super(View_AppComponent0,renderType_AppComponent,import7.ViewType.COMPONENT,viewUtils,parentView,parentIndex,parentElement,import8.ChangeDetectorStatus.CheckAlways);
    this._expr_13 = import1.UNINITIALIZED;
    this._arr_16 = import3.pureProxy3((p0:any,p1:any,p2:any):any[] =&> {
    return [
    p0,
    p1,
    p2
    ]
    ;
    });
    this._expr_17 = import1.UNINITIALIZED;
    }
    createInternal(rootSelector:string):import10.ComponentRef& {
    const parentRenderNode:any = this.renderer.createViewRoot(this.parentElement);
    this._el_0 = import3.createRenderElement(this.renderer,parentRenderNode,"h1",import3.EMPTY_INLINE_ARRAY,(null as any));
    this._text_1 = this.renderer.createText(this._el_0,"",(null as any));
    this._text_2 = this.renderer.createText(parentRenderNode,"
    ",(null as any));
    this._el_3 = import3.createRenderElement(this.renderer,parentRenderNode,"ul",new import3.InlineArray2(2,"class","list"),(null as any));
    this._text_4 = this.renderer.createText(this._el_3,"
    ",(null as any));
    this._anchor_5 = this.renderer.createTemplateAnchor(this._el_3,(null as any));
    this._vc_5 = new import6.ViewContainer(5,3,this,this._anchor_5);
    this._TemplateRef_5_5 = new import14.TemplateRef_(this,5,this._anchor_5);
    this._NgFor_5_6 = new import12.Wrapper_NgFor(this._vc_5.vcRef,this._TemplateRef_5_5,this.parentView.injectorGet(import15.IterableDiffers,this.parentIndex),this.ref);
    this._text_6 = this.renderer.createText(this._el_3,"
    ",(null as any));
    this._text_7 = this.renderer.createText(parentRenderNode,"
    ",(null as any));
    this._el_8 = import3.createRenderElement(this.renderer,parentRenderNode,"input",import3.EMPTY_INLINE_ARRAY,(null as any));
    this._text_9 = this.renderer.createText(parentRenderNode,"
    ",(null as any));
    this._pipe_uppercase_0 = new import13.UpperCasePipe();
    this._pipe_uppercase_0_0 = import3.pureProxy1(this._pipe_uppercase_0.transform.bind(this._pipe_uppercase_0));
    var disposable_0:Function = import3.subscribeToRenderElement(this,this._el_8,new import3.InlineArray2(2,"change",(null as any)),this.eventHandler(this.handleEvent_8));
    this.init((null as any),((&this.renderer).directRenderer? (null as any): [
    this._el_0,
    this._text_1,
    this._text_2,
    this._el_3,
    this._text_4,
    this._anchor_5,
    this._text_6,
    this._text_7,
    this._el_8,
    this._text_9
    ]
    ),[disposable_0]);
    return (null as any);
    }
    injectorGetInternal(token:any,requestNodeIndex:number,notFoundResult:any):any {
    if (((token === import14.TemplateRef) (5 === requestNodeIndex))) { return this._TemplateRef_5_5; }
    if (((token === import16.NgFor) (5 === requestNodeIndex))) { return this._NgFor_5_6.context; }
    return notFoundResult;
    }
    detectChangesInternal(throwOnChange:boolean):void {
    const valUnwrapper:any = new import1.ValueUnwrapper();
    const currVal_5_0_0:any = this._arr_16(1,2,3);
    this._NgFor_5_6.check_ngForOf(currVal_5_0_0,throwOnChange,false);
    this._NgFor_5_6.ngDoCheck(this,this._anchor_5,throwOnChange);
    this._vc_5.detectChangesInNestedViews(throwOnChange);
    valUnwrapper.reset();
    const currVal_13:any = import3.inlineInterpolate(1,"
    ",valUnwrapper.unwrap(import3.castByValue(this._pipe_uppercase_0_0,this._pipe_uppercase_0.transform)(this.context.title)),"
    ");
    if ((valUnwrapper.hasWrappedValue || import3.checkBinding(throwOnChange,this._expr_13,currVal_13))) {
    this.renderer.setText(this._text_1,currVal_13);
    this._expr_13 = currVal_13;
    }
    const currVal_17:any = this.context.title;
    if (import3.checkBinding(throwOnChange,this._expr_17,currVal_17)) {
    this.renderer.setElementProperty(this._el_8,"value",currVal_17);
    this._expr_17 = currVal_17;
    }
    }
    destroyInternal():void {
    this._vc_5.destroyNestedViews();
    }
    createEmbeddedViewInternal(nodeIndex:number):import2.AppView& {
    if ((nodeIndex == 5)) { return new View_AppComponent1(this.viewUtils,this,5,this._anchor_5,this._vc_5); }
    return (null as any);
    }
    handleEvent_8(eventName:string,$event:any):boolean {
    this.markPathToRootAsCheckOnce();
    var result:boolean = true;
    if ((eventName == "change")) {
    const pd_sub_0:any = ((&this.context.onChange()) !== false);
    result = (pd_sub_0 result);
    }
    return result;
    }
    }

    以及一個額外的 View_AppComponent1 類(對應於 NgFor 所在的 template/ng-template 標籤):

    class View_AppComponent1 extends import2.AppView& {
    _el_0:any;
    _text_1:any;
    /*private*/ _expr_2:any;
    constructor(viewUtils:import3.ViewUtils,parentView:import2.AppView&,parentIndex:number,parentElement:any,declaredViewContainer:import6.ViewContainer) {
    super(View_AppComponent1,renderType_AppComponent,import7.ViewType.EMBEDDED,viewUtils,parentView,parentIndex,parentElement,import8.ChangeDetectorStatus.CheckAlways,declaredViewContainer);
    this._expr_2 = import1.UNINITIALIZED;
    }
    createInternal(rootSelector:string):import10.ComponentRef& {
    this._el_0 = import3.createRenderElement(this.renderer,(null as any),"li",new import3.InlineArray2(2,"class","line"),(null as any));
    this._text_1 = this.renderer.createText(this._el_0,"",(null as any));
    this.init(this._el_0,((&this.renderer).directRenderer? (null as any): [
    this._el_0,
    this._text_1
    ]
    ),(null as any));
    return (null as any);
    }
    detectChangesInternal(throwOnChange:boolean):void {
    const currVal_2:any = import3.inlineInterpolate(1,"Line ",this.context.$implicit,"");
    if (import3.checkBinding(throwOnChange,this._expr_2,currVal_2)) {
    this.renderer.setText(this._text_1,currVal_2);
    this._expr_2 = currVal_2;
    }
    }
    visitRootNodesInternal(cb:any,ctx:any):void {
    cb(this._el_0,ctx);
    }
    }

    在 createInternal 中,我們看到組件模版被編譯為視圖操作(類似於 DOM 操作,不過進行了抽象)之後的調用過程。

    在 injectorGetInternal 中,定義了視圖中相關依賴的查詢過程。

    在 detectChangesInternal 中,調用了 NgFor 對 ngForOf 這個 @Input() 的檢查和其定義的 ngDoCheck 生命周期;在計算 h1 中表達式內容的過程中調用了 UpperCasePipe 的 transform 方法,如果總體結果變化,則更新 h1 的內容;檢查 input 的 value 綁定,如果發生變化,則更新其 value 屬性。

    在 createEmbeddedViewInternal 中,定義了創建 View_AppComponent1(NgFor 所在的 template/ng-template 模版對應的)視圖的過程。

    在 handleEvent_8 中,檢查事件名是否為 change,如果是則調用 input 上的 onChange 回調方法。

    於是,我們又了解了模版的編譯結果、模版中的變化檢測(臟檢測)過程,模版中的事件綁定處理方式、template/ng-template 標籤的處理、Pipe 的應用原理。

    受益於 NgFor 的編譯結果:

    export class Wrapper_NgFor {
    /*private*/ _eventHandler:Function;
    context:import0.NgFor;
    /*private*/ _changed:boolean;
    /*private*/ _changes:{[key: string]:any};
    /*private*/ _expr_0:any;
    /*private*/ _expr_1:any;
    /*private*/ _expr_2:any;
    constructor(p0:any,p1:any,p2:any,p3:any) {
    this._changed = false;
    this._changes = {};
    this.context = new import0.NgFor(p0,p1,p2,p3);
    this._expr_0 = import1.UNINITIALIZED;
    this._expr_1 = import1.UNINITIALIZED;
    this._expr_2 = import1.UNINITIALIZED;
    }
    ngOnDetach(view:import2.AppView&,componentView:import2.AppView&,el:any):void {
    }
    ngOnDestroy():void {
    }
    check_ngForOf(currValue:any,throwOnChange:boolean,forceUpdate:boolean):void {
    if ((forceUpdate || import3.checkBinding(throwOnChange,this._expr_0,currValue))) {
    this._changed = true;
    this.context.ngForOf = currValue;
    this._changes["ngForOf"] = new import1.SimpleChange(this._expr_0,currValue);
    this._expr_0 = currValue;
    }
    }
    check_ngForTrackBy(currValue:any,throwOnChange:boolean,forceUpdate:boolean):void {
    if ((forceUpdate || import3.checkBinding(throwOnChange,this._expr_1,currValue))) {
    this._changed = true;
    this.context.ngForTrackBy = currValue;
    this._changes["ngForTrackBy"] = new import1.SimpleChange(this._expr_1,currValue);
    this._expr_1 = currValue;
    }
    }
    check_ngForTemplate(currValue:any,throwOnChange:boolean,forceUpdate:boolean):void {
    if ((forceUpdate || import3.checkBinding(throwOnChange,this._expr_2,currValue))) {
    this._changed = true;
    this.context.ngForTemplate = currValue;
    this._changes["ngForTemplate"] = new import1.SimpleChange(this._expr_2,currValue);
    this._expr_2 = currValue;
    }
    }
    ngDoCheck(view:import2.AppView&,el:any,throwOnChange:boolean):boolean {
    var changed:any = this._changed;
    this._changed = false;
    if (!throwOnChange) {
    if (changed) {
    this.context.ngOnChanges(this._changes);
    this._changes = {};
    }
    this.context.ngDoCheck();
    }
    return changed;
    }
    checkHost(view:import2.AppView&,componentView:import2.AppView&,el:any,throwOnChange:boolean):void {
    }
    handleEvent(eventName:string,$event:any):boolean {
    var result:boolean = true;
    return result;
    }
    subscribe(view:import2.AppView&,_eventHandler:any):void {
    this._eventHandler = _eventHandler;
    }
    }

    我們又知道了每個 Directive 也會編譯出對應的包裝類,並且每個 @Input() 輸入綁定都會編譯成獨立的變化檢測方法

    然後再來關注一下 NgModule 的 app.module.ngfactory.ts:

    class AppModuleInjector extends import0.NgModuleInjector& {
    _CommonModule_0:import2.CommonModule;
    _ApplicationModule_1:import3.ApplicationModule;
    _BrowserModule_2:import4.BrowserModule;
    _InternalFormsSharedModule_3:import5.InternalFormsSharedModule;
    _FormsModule_4:import6.FormsModule;
    _HttpModule_5:import7.HttpModule;
    _AppModule_6:import1.AppModule;
    __LOCALE_ID_7:any;
    __NgLocalization_8:import8.NgLocaleLocalization;
    _ErrorHandler_9:any;
    _ApplicationInitStatus_10:import9.ApplicationInitStatus;
    _Testability_11:import10.Testability;
    _ApplicationRef__12:import11.ApplicationRef_;
    __ApplicationRef_13:any;
    __Compiler_14:import12.Compiler;
    __APP_ID_15:any;
    __DOCUMENT_16:any;
    __HAMMER_GESTURE_CONFIG_17:import13.HammerGestureConfig;
    __EVENT_MANAGER_PLUGINS_18:any[];
    __EventManager_19:import14.EventManager;
    _DomSharedStylesHost_20:import15.DomSharedStylesHost;
    __AnimationDriver_21:any;
    __DomRootRenderer_22:import16.DomRootRenderer_;
    __RootRenderer_23:any;
    __DomSanitizer_24:import17.DomSanitizerImpl;
    __Sanitizer_25:any;
    __AnimationQueue_26:import18.AnimationQueue;
    __ViewUtils_27:import19.ViewUtils;
    __IterableDiffers_28:any;
    __KeyValueDiffers_29:any;
    __SharedStylesHost_30:any;
    __Title_31:import20.Title;
    __RadioControlRegistry_32:import21.RadioControlRegistry;
    __BrowserXhr_33:import22.BrowserXhr;
    __ResponseOptions_34:import23.BaseResponseOptions;
    __XSRFStrategy_35:any;
    __XHRBackend_36:import24.XHRBackend;
    __RequestOptions_37:import25.BaseRequestOptions;
    __Http_38:any;
    constructor(parent:import26.Injector) {
    super(parent,[import27.AppComponentNgFactory],[import27.AppComponentNgFactory]);
    }
    get _LOCALE_ID_7():any {
    if ((this.__LOCALE_ID_7 == null)) { (this.__LOCALE_ID_7 = import3._localeFactory(this.parent.get(import28.LOCALE_ID,(null as any)))); }
    return this.__LOCALE_ID_7;
    }
    get _NgLocalization_8():import8.NgLocaleLocalization {
    if ((this.__NgLocalization_8 == null)) { (this.__NgLocalization_8 = new import8.NgLocaleLocalization(this._LOCALE_ID_7)); }
    return this.__NgLocalization_8;
    }
    get _ApplicationRef_13():any {
    if ((this.__ApplicationRef_13 == null)) { (this.__ApplicationRef_13 = this._ApplicationRef__12); }
    return this.__ApplicationRef_13;
    }
    get _Compiler_14():import12.Compiler {
    if ((this.__Compiler_14 == null)) { (this.__Compiler_14 = new import12.Compiler()); }
    return this.__Compiler_14;
    }
    get _APP_ID_15():any {
    if ((this.__APP_ID_15 == null)) { (this.__APP_ID_15 = import29._appIdRandomProviderFactory()); }
    return this.__APP_ID_15;
    }
    get _DOCUMENT_16():any {
    if ((this.__DOCUMENT_16 == null)) { (this.__DOCUMENT_16 = import4._document()); }
    return this.__DOCUMENT_16;
    }
    get _HAMMER_GESTURE_CONFIG_17():import13.HammerGestureConfig {
    if ((this.__HAMMER_GESTURE_CONFIG_17 == null)) { (this.__HAMMER_GESTURE_CONFIG_17 = new import13.HammerGestureConfig()); }
    return this.__HAMMER_GESTURE_CONFIG_17;
    }
    get _EVENT_MANAGER_PLUGINS_18():any[] {
    if ((this.__EVENT_MANAGER_PLUGINS_18 == null)) { (this.__EVENT_MANAGER_PLUGINS_18 = [
    new import30.DomEventsPlugin(),
    new import31.KeyEventsPlugin(),
    new import13.HammerGesturesPlugin(this._HAMMER_GESTURE_CONFIG_17)
    ]
    ); }
    return this.__EVENT_MANAGER_PLUGINS_18;
    }
    get _EventManager_19():import14.EventManager {
    if ((this.__EventManager_19 == null)) { (this.__EventManager_19 = new import14.EventManager(this._EVENT_MANAGER_PLUGINS_18,this.parent.get(import32.NgZone))); }
    return this.__EventManager_19;
    }
    get _AnimationDriver_21():any {
    if ((this.__AnimationDriver_21 == null)) { (this.__AnimationDriver_21 = import4._resolveDefaultAnimationDriver()); }
    return this.__AnimationDriver_21;
    }
    get _DomRootRenderer_22():import16.DomRootRenderer_ {
    if ((this.__DomRootRenderer_22 == null)) { (this.__DomRootRenderer_22 = new import16.DomRootRenderer_(this._DOCUMENT_16,this._EventManager_19,this._DomSharedStylesHost_20,this._AnimationDriver_21,this._APP_ID_15)); }
    return this.__DomRootRenderer_22;
    }
    get _RootRenderer_23():any {
    if ((this.__RootRenderer_23 == null)) { (this.__RootRenderer_23 = import33._createConditionalRootRenderer(this._DomRootRenderer_22,this.parent.get(import33.NgProbeToken,(null as any)),this.parent.get(import11.NgProbeToken,(null as any)))); }
    return this.__RootRenderer_23;
    }
    get _DomSanitizer_24():import17.DomSanitizerImpl {
    if ((this.__DomSanitizer_24 == null)) { (this.__DomSanitizer_24 = new import17.DomSanitizerImpl()); }
    return this.__DomSanitizer_24;
    }
    get _Sanitizer_25():any {
    if ((this.__Sanitizer_25 == null)) { (this.__Sanitizer_25 = this._DomSanitizer_24); }
    return this.__Sanitizer_25;
    }
    get _AnimationQueue_26():import18.AnimationQueue {
    if ((this.__AnimationQueue_26 == null)) { (this.__AnimationQueue_26 = new import18.AnimationQueue(this.parent.get(import32.NgZone))); }
    return this.__AnimationQueue_26;
    }
    get _ViewUtils_27():import19.ViewUtils {
    if ((this.__ViewUtils_27 == null)) { (this.__ViewUtils_27 = new import19.ViewUtils(this._RootRenderer_23,this._Sanitizer_25,this._AnimationQueue_26)); }
    return this.__ViewUtils_27;
    }
    get _IterableDiffers_28():any {
    if ((this.__IterableDiffers_28 == null)) { (this.__IterableDiffers_28 = import3._iterableDiffersFactory()); }
    return this.__IterableDiffers_28;
    }
    get _KeyValueDiffers_29():any {
    if ((this.__KeyValueDiffers_29 == null)) { (this.__KeyValueDiffers_29 = import3._keyValueDiffersFactory()); }
    return this.__KeyValueDiffers_29;
    }
    get _SharedStylesHost_30():any {
    if ((this.__SharedStylesHost_30 == null)) { (this.__SharedStylesHost_30 = this._DomSharedStylesHost_20); }
    return this.__SharedStylesHost_30;
    }
    get _Title_31():import20.Title {
    if ((this.__Title_31 == null)) { (this.__Title_31 = new import20.Title()); }
    return this.__Title_31;
    }
    get _RadioControlRegistry_32():import21.RadioControlRegistry {
    if ((this.__RadioControlRegistry_32 == null)) { (this.__RadioControlRegistry_32 = new import21.RadioControlRegistry()); }
    return this.__RadioControlRegistry_32;
    }
    get _BrowserXhr_33():import22.BrowserXhr {
    if ((this.__BrowserXhr_33 == null)) { (this.__BrowserXhr_33 = new import22.BrowserXhr()); }
    return this.__BrowserXhr_33;
    }
    get _ResponseOptions_34():import23.BaseResponseOptions {
    if ((this.__ResponseOptions_34 == null)) { (this.__ResponseOptions_34 = new import23.BaseResponseOptions()); }
    return this.__ResponseOptions_34;
    }
    get _XSRFStrategy_35():any {
    if ((this.__XSRFStrategy_35 == null)) { (this.__XSRFStrategy_35 = import7._createDefaultCookieXSRFStrategy()); }
    return this.__XSRFStrategy_35;
    }
    get _XHRBackend_36():import24.XHRBackend {
    if ((this.__XHRBackend_36 == null)) { (this.__XHRBackend_36 = new import24.XHRBackend(this._BrowserXhr_33,this._ResponseOptions_34,this._XSRFStrategy_35)); }
    return this.__XHRBackend_36;
    }
    get _RequestOptions_37():import25.BaseRequestOptions {
    if ((this.__RequestOptions_37 == null)) { (this.__RequestOptions_37 = new import25.BaseRequestOptions()); }
    return this.__RequestOptions_37;
    }
    get _Http_38():any {
    if ((this.__Http_38 == null)) { (this.__Http_38 = import7.httpFactory(this._XHRBackend_36,this._RequestOptions_37)); }
    return this.__Http_38;
    }
    createInternal():import1.AppModule {
    this._CommonModule_0 = new import2.CommonModule();
    this._ApplicationModule_1 = new import3.ApplicationModule();
    this._BrowserModule_2 = new import4.BrowserModule(this.parent.get(import4.BrowserModule,(null as any)));
    this._InternalFormsSharedModule_3 = new import5.InternalFormsSharedModule();
    this._FormsModule_4 = new import6.FormsModule();
    this._HttpModule_5 = new import7.HttpModule();
    this._AppModule_6 = new import1.AppModule();
    this._ErrorHandler_9 = import4.errorHandler();
    this._ApplicationInitStatus_10 = new import9.ApplicationInitStatus(this.parent.get(import9.APP_INITIALIZER,(null as any)));
    this._Testability_11 = new import10.Testability(this.parent.get(import32.NgZone));
    this._ApplicationRef__12 = new import11.ApplicationRef_(this.parent.get(import32.NgZone),this.parent.get(import34.Console),this,this._ErrorHandler_9,this,this._ApplicationInitStatus_10,this.parent.get(import10.TestabilityRegistry,(null as any)),this._Testability_11);
    this._DomSharedStylesHost_20 = new import15.DomSharedStylesHost(this._DOCUMENT_16);
    return this._AppModule_6;
    }
    getInternal(token:any,notFoundResult:any):any {
    if ((token === import2.CommonModule)) { return this._CommonModule_0; }
    if ((token === import3.ApplicationModule)) { return this._ApplicationModule_1; }
    if ((token === import4.BrowserModule)) { return this._BrowserModule_2; }
    if ((token === import5.InternalFormsSharedModule)) { return this._InternalFormsSharedModule_3; }
    if ((token === import6.FormsModule)) { return this._FormsModule_4; }
    if ((token === import7.HttpModule)) { return this._HttpModule_5; }
    if ((token === import1.AppModule)) { return this._AppModule_6; }
    if ((token === import28.LOCALE_ID)) { return this._LOCALE_ID_7; }
    if ((token === import8.NgLocalization)) { return this._NgLocalization_8; }
    if ((token === import35.ErrorHandler)) { return this._ErrorHandler_9; }
    if ((token === import9.ApplicationInitStatus)) { return this._ApplicationInitStatus_10; }
    if ((token === import10.Testability)) { return this._Testability_11; }
    if ((token === import11.ApplicationRef_)) { return this._ApplicationRef__12; }
    if ((token === import11.ApplicationRef)) { return this._ApplicationRef_13; }
    if ((token === import12.Compiler)) { return this._Compiler_14; }
    if ((token === import29.APP_ID)) { return this._APP_ID_15; }
    if ((token === import36.DOCUMENT)) { return this._DOCUMENT_16; }
    if ((token === import13.HAMMER_GESTURE_CONFIG)) { return this._HAMMER_GESTURE_CONFIG_17; }
    if ((token === import14.EVENT_MANAGER_PLUGINS)) { return this._EVENT_MANAGER_PLUGINS_18; }
    if ((token === import14.EventManager)) { return this._EventManager_19; }
    if ((token === import15.DomSharedStylesHost)) { return this._DomSharedStylesHost_20; }
    if ((token === import37.AnimationDriver)) { return this._AnimationDriver_21; }
    if ((token === import16.DomRootRenderer)) { return this._DomRootRenderer_22; }
    if ((token === import38.RootRenderer)) { return this._RootRenderer_23; }
    if ((token === import17.DomSanitizer)) { return this._DomSanitizer_24; }
    if ((token === import39.Sanitizer)) { return this._Sanitizer_25; }
    if ((token === import18.AnimationQueue)) { return this._AnimationQueue_26; }
    if ((token === import19.ViewUtils)) { return this._ViewUtils_27; }
    if ((token === import40.IterableDiffers)) { return this._IterableDiffers_28; }
    if ((token === import41.KeyValueDiffers)) { return this._KeyValueDiffers_29; }
    if ((token === import15.SharedStylesHost)) { return this._SharedStylesHost_30; }
    if ((token === import20.Title)) { return this._Title_31; }
    if ((token === import21.RadioControlRegistry)) { return this._RadioControlRegistry_32; }
    if ((token === import22.BrowserXhr)) { return this._BrowserXhr_33; }
    if ((token === import23.ResponseOptions)) { return this._ResponseOptions_34; }
    if ((token === import42.XSRFStrategy)) { return this._XSRFStrategy_35; }
    if ((token === import24.XHRBackend)) { return this._XHRBackend_36; }
    if ((token === import25.RequestOptions)) { return this._RequestOptions_37; }
    if ((token === import43.Http)) { return this._Http_38; }
    return notFoundResult;
    }
    destroyInternal():void {
    this._ApplicationRef__12.ngOnDestroy();
    this._DomSharedStylesHost_20.ngOnDestroy();
    }
    }

    可以看出,模塊注入器和組件注入器的工作方式相同,只是依賴項可能更為豐富。不止是模塊自身元數據中聲明的 providers,所有被依賴模版的 providers 也會一同編譯於此。這也就是為什麼說所有非延遲載入的 NgModule 會共用同一個注入器,以及延遲載入的 NgModule 中儘可能不要重複聲明 providers(只有 forRoot 方法提供 providers,否則只提供模塊的其它基礎部分)

    這裡也就是一些 Angular 文章中所說的「Angular 會生成面相 JavaScript 引擎友好的代碼」,翻譯成人話也就是大量使用 if 的代碼(大霧)。不過這樣的收益是可觀的,Angular 應用的工作高度依賴於依賴注入,而通過 if 的方式,能有效受益於分支預測和內聯優化(第一次以後條件永真)。

    這也解釋了為什麼 ReflectiveInjector 會叫做 ReflectiveInjector,因為正常的 Injector 是完全 non-reflective 的,幾乎沒有用到任何 JavaScript 的動態特性,這也保證了 Angular 應用啟動過程中的高性能。

    最後,我們再來看看 ngc 所編譯出的 JavaScript 與 tsc 有何不同,app.component.js 的內容為:

    export var AppComponent = (function () {
    function AppComponent(location, vcRef) {
    this.title = "app works!";
    this.hasAbcClass = true;
    }
    AppComponent.prototype.onClick = function () { };
    AppComponent.prototype.onKeydown = function () { };
    AppComponent.prototype.ngOnInit = function () { };
    AppComponent.prototype.ngOnChanges = function (changes) { };
    AppComponent.prototype.ngOnDestroy = function () { };
    AppComponent.prototype.ngDoCheck = function () { };
    AppComponent.prototype.ngAfterViewInit = function () { };
    AppComponent.prototype.ngAfterContentInit = function () { };
    AppComponent.prototype.ngAfterViewChecked = function () { };
    AppComponent.prototype.ngAfterContentChecked = function () { };
    AppComponent.prototype.onChange = function () { };
    AppComponent.decorators = [
    { type: Component, args: [{
    selector: "app-root",
    templateUrl: "./app.component.html",
    styleUrls: ["./app.component.css"]
    },] },
    ];
    /** @nocollapse */
    AppComponent.ctorParameters = function () { return [
    { type: Location, },
    { type: ViewContainerRef, },
    ]; };
    AppComponent.propDecorators = {
    "hasAbcClass": [{ type: HostBinding, args: ["class.abc",] },],
    "onClick": [{ type: HostListener, args: ["click",] },],
    "onKeydown": [{ type: HostListener, args: ["keydown",] },],
    };
    return AppComponent;
    }());

    我們可能會感到奇怪,Decorator 並沒有編譯成函數調用,而是編譯成了類的靜態屬性。因為只是保存元數據,為此引入的額外的函數調用和 Polyfill(reflect-metadata)是不必要的,所以在 AOT 模式下這裡(對 Angular 自己的 Decorator)並沒有遵循 Decorator 的語義,即便 Decorator 提案發生變化(已經變了,Decorator 現在在 Stage 2,而 TypeScript 和 Babel 都還是實現的 Stage 0 的提案版本)也不受影響。

    既然提到了 AOT,那麼 JIT 下又是什麼情況呢?實際上也就是把這個編譯過程放到瀏覽器中了,所以不僅需要的額外的依賴大小(@angular/compiler),還有額外的啟動時間(編譯過程),不適用於實際生產環境。

    JIT 下編譯出的代碼為 ES5,採用 new Function() 的方式執行,可以在瀏覽器的調試工具中看到生成的代碼:

    了解完了「編譯時」的代碼以後,再去看「運行時」的代碼是不是就覺得很簡單了呢?

    有了「編譯出的內容」之後,即便不調試,靠著靜態代碼分析也基本能掌握大部分核心過程了。相反,如果直接去看源碼,這部分內容是找不到對應物的,反而會產生疑惑。(如果能通過編譯器源碼腦補結果的話當我沒說)

    ---

    《Build your own Angular》 這樣的書估計是出不出來了,因為至少一半的章節都需要用來教編譯原理。。


    官方文檔講的還是挺詳細的

    也可以看看github上面的源碼


    謝邀,可以去github上看下源碼實現


    推薦閱讀:

angular2中數據狀態管理方案有哪些?
PC 前端是不是沒希望了?
Angular和React哪個更有前景?
angular2要用typescript,是不是現在該提前做好準備了,開擼typescript?
如何評價尤雨溪向大漠窮秋的領導就Vue與Angular"爭論"的舉報?

TAG:Angular? |