譯文 | Angular中的AoT編譯

前兩天冠王,Jigsaw七巧板上來(lái)了個(gè)issue https://github.com/rdkmaster/jigsaw/issues/113(https://github.com/jackjoy) 在issue中提到了一篇介紹Angular AoT的文章,我看了一下卖子,覺(jué)得講的非常好胜茧,還涉及到一些Angular編譯原理的內(nèi)容。于是打算翻譯一下,讓大伙都能夠讀一讀亏推,多了解一點(diǎn)AoT知識(shí)学赛。

文中的第一人稱“我”均指代作者本人(http://blog.mgechev.com)

原文地址是 http://blog.mgechev.com/2016/08/14/ahead-of-time-compilation-angular-offline-precompilation/

最近我給angular-seed增加了對(duì)Ahead-of-Time(AoT)編譯的支持,這引來(lái)了不少關(guān)于這個(gè)新特性的問(wèn)題吞杭。我們從下面這些話題開(kāi)始來(lái)回答這些問(wèn)題:

  • 為什么Angular需要編譯盏浇?
  • 什么東西會(huì)被編譯?
  • 他們是如何被編譯的芽狗?
  • 編譯發(fā)生在什么時(shí)候绢掰?JiT vs AoT
  • 我們從AoT中獲得了什么?
  • AoT編譯是如何工作的童擎?
  • 我們使用AoT和JiT的代價(jià)是什么滴劲?

為什么Angular需要編譯?

這個(gè)問(wèn)題的簡(jiǎn)短回答是:編譯可以讓Angular應(yīng)用達(dá)到更高層度的運(yùn)行效率顾复,我所說(shuō)的效率班挖,主要是指的性能提升,但也包括電池節(jié)能和節(jié)省流量芯砸。

AngularJs1.x 有一個(gè)實(shí)現(xiàn)渲染和變化檢測(cè)的很動(dòng)態(tài)的方式萧芙,比如AngularJs1.x的編譯器非常通用,它被設(shè)計(jì)為任何模板實(shí)現(xiàn)一系列的動(dòng)態(tài)計(jì)算假丧,雖然它在通常情況下運(yùn)行的很好双揪,但是JS虛擬機(jī)的動(dòng)態(tài)特性讓一些低層次的計(jì)算優(yōu)化變得很困難。由于js虛擬機(jī)無(wú)法理解那些我們作為臟檢查的上下文對(duì)象(術(shù)語(yǔ)為scope)的形態(tài)包帚,虛擬機(jī)的內(nèi)聯(lián)緩存常常不精確渔期,這導(dǎo)致了運(yùn)行效率的下降

譯者:scope是AngularJs1.x中的一個(gè)重要對(duì)象渴邦,他是AngularJs1.x用于計(jì)算模板的上下文疯趟。

Angular2+采用了一個(gè)不同的方式。在給每個(gè)組件做渲染和變化檢測(cè)的時(shí)候几莽,它不再使用同一套邏輯迅办,框架在運(yùn)行時(shí)或者編譯時(shí)會(huì)生成對(duì)js虛擬機(jī)友好的代碼。這些友好的代碼可以讓js虛擬機(jī)在屬性訪問(wèn)的緩存章蚣,執(zhí)行變化檢查站欺,進(jìn)行渲染的邏輯執(zhí)行的快的多

舉個(gè)例子纤垂,看看下面的代碼:

// ...
Scope.prototype.$digest = function () {
  'use strict';
  var dirty, watcher, current, i;
  do {
    dirty = false;
    for (i = 0; i < this.$$watchers.length; i += 1) {
      watcher = this.$$watchers[i];
      current = this.$eval(watcher.exp);
      if (!Utils.equals(watcher.last, current)) {
        watcher.last = Utils.clone(current);
        dirty = true;
        watcher.fn(current);
      }
    }
  } while (dirty);
  for (i = 0; i < this.$$children.length; i += 1) {
    this.$$children[i].$digest();
  }
};
// ...

這個(gè)代碼片段來(lái)自《輕量級(jí)angularJs1.x實(shí)現(xiàn)》一文矾策。這些代碼實(shí)現(xiàn)了對(duì)scope樹(shù)做深度優(yōu)先搜索,目的是為了尋找綁定數(shù)據(jù)的變化峭沦,這個(gè)方法對(duì)任何指令都生效贾虽。這些代碼顯然比下面這些直接指定檢查的代碼慢:

// ...
var currVal_6 = this.context.newName;
if (import4.checkBinding(throwOnChange, this._expr_6, currVal_6)) {
    this._NgModel_5_5.model = currVal_6;
    if ((changes === null)) {
        (changes = {});
    }
    changes['model'] = new import7.SimpleChange(this._expr_6, currVal_6);
    this._expr_6 = currVal_6;
}
this.detectContentChildrenChanges(throwOnChange);
// ...

譯者:

《輕量級(jí)angularJs1.x實(shí)現(xiàn)》的地址是 <https://github.com/mgechev/light-angularjs/blob/master/lib/Scope.js#L61-L79

這里一下子提及了angularJs1.x的好幾個(gè)概念,包括scope吼鱼,數(shù)據(jù)綁定蓬豁,指令绰咽。不熟悉angularJs1.x的同學(xué)理解起來(lái)費(fèi)勁,想弄懂的話地粪,自行搜索吧取募。個(gè)人認(rèn)為可以無(wú)視,畢竟這個(gè)文章的重點(diǎn)不是在這里蟆技。你就認(rèn)為Angular2+的處理方式比angularJs1.x牛逼很多就好了玩敏,哈哈。

上面代碼包含了一個(gè)來(lái)自angular-seed的某個(gè)編譯后的組件的代碼质礼,這些代碼是由編譯器生成的旺聚,包含了一個(gè) detectChangesInternal 方法的實(shí)現(xiàn)。Angular框架通過(guò)直接屬性訪問(wèn)的方式讀取了數(shù)據(jù)綁定中的某些值眶蕉,并且采用了最高效的方式與新的值做比較砰粹。一旦Angular框架發(fā)現(xiàn)這些值發(fā)生了變化,它就立即更新只受這些數(shù)據(jù)波及的DOM元素妻坝。

在回答了“為什么Angular需要編譯”這個(gè)問(wèn)題的同時(shí)伸眶,我們同時(shí)也回答了“什么東西會(huì)被編譯”這個(gè)問(wèn)題。我們希望把組件的模板編譯成一個(gè)JS類刽宪,這些類包含了在綁定的數(shù)據(jù)中檢測(cè)變化和渲染UI的邏輯。通過(guò)這個(gè)方式界酒,我們和潛在的平臺(tái)解耦了圣拄。換句話說(shuō),通過(guò)對(duì)渲染器采取了不同的實(shí)現(xiàn)毁欣,我們?cè)诓粚?duì)代碼做任何的修改的前提下庇谆,就可以對(duì)同一個(gè)以AoT方式編譯的組件做不同的渲染。比如凭疮,上述代碼中的組件還可以直接用在NativeScript中饭耳,這是由于這些不同的渲染器都能夠理解編譯后的組件。

編譯發(fā)生在什么時(shí)候执解?JiT 還是 AoT

Angular編譯器最cool的一點(diǎn)是它可以在頁(yè)面運(yùn)行時(shí)(例如在用戶的瀏覽器內(nèi))啟動(dòng)寞肖,也可以作為構(gòu)建的一個(gè)步驟在頁(yè)面的編譯時(shí)啟動(dòng)。這主要得益于Angular的可移植性:我們可以在任何的平臺(tái)的JS虛擬機(jī)上運(yùn)行Angular衰腌,所以我們完全可以在瀏覽器和NodeJs中運(yùn)行它新蟆。

JiT編譯模式的流程

一個(gè)典型的非AoT應(yīng)用的開(kāi)發(fā)流程大概是:

  • 使用TypeScript開(kāi)發(fā)Angular應(yīng)用
  • 使用tsc來(lái)編譯這個(gè)應(yīng)用的ts代碼
  • 打包
  • 壓縮
  • 部署

一旦把a(bǔ)pp部署好了,并且用戶在瀏覽器中打開(kāi)了這個(gè)app右蕊,下面這些事情會(huì)逐一進(jìn)行:

  • 瀏覽器下載js代碼
  • Angular啟動(dòng)
  • Angular在瀏覽器中開(kāi)始JiT編譯的過(guò)程琼稻,例如生成app中各個(gè)組件的js代碼
  • 應(yīng)用頁(yè)面得以渲染

AoT編譯模式的流程

相對(duì)的,使用AoT模式的應(yīng)用的開(kāi)發(fā)流程是:

  • 使用TypeScript開(kāi)發(fā)Angular應(yīng)用
  • 使用ngc來(lái)編譯應(yīng)用
    • 使用Angular編譯器對(duì)模板進(jìn)行編譯饶囚,生成TypeScript代碼
    • TypesScript代碼編譯為JavaScript代碼
  • 打包
  • 壓縮
  • 部署

雖然前面的過(guò)程稍稍復(fù)雜帕翻,但是用戶這一側(cè)的事情就變簡(jiǎn)單了:

  • 下載所以代碼
  • Angular啟動(dòng)
  • 應(yīng)用頁(yè)面得以渲染

如你所見(jiàn)鸠补,第三步被省略掉了,這意味著頁(yè)面打開(kāi)更快嘀掸,用戶體驗(yàn)也更好莫鸭。類似Angular-cli和Angular-seed這樣的工具可以讓整個(gè)編譯過(guò)程變的非常的自動(dòng)化。

概括起來(lái)横殴,Angular中的Jit和AoT的主要區(qū)別是:

  • 編譯過(guò)程發(fā)生的時(shí)機(jī)
  • JiT生成的是JS代碼被因,而AoT生成的是TS代碼。這主要是因?yàn)镴iT是在瀏覽器中進(jìn)行的衫仑,它完全沒(méi)必要生成TS代碼梨与,而是直接生產(chǎn)了JS代碼。

你可以在我的Github賬號(hào)中找到一個(gè)最小的AoT編譯demo文狱,鏈接在這里 https://github.com/mgechev/angular2-ngc-rollup-build

深入AoT編譯

這個(gè)小節(jié)回答了這些問(wèn)題:

  • AoT編譯過(guò)程產(chǎn)生了什么文件粥鞋?
  • 這些產(chǎn)生的文件的上下文是什么?
  • 如何開(kāi)發(fā)出AoT友好又有良好封裝的代碼瞄崇?

對(duì)@angular/compiler的代碼一行一行的解釋沒(méi)太大意義呻粹,因此我們僅僅來(lái)快速過(guò)一下編譯的過(guò)程。如果你對(duì)編譯器的詞法分析過(guò)程苏研,解析和生成代碼過(guò)程等感興趣等浊,你可以讀一讀Tobias Bosch的《Angular2編譯器》一文,或者它的膠片摹蘑。

譯者:

《Angular2編譯器》一文鏈接 https://www.youtube.com/watch?v=kW9cJsvcsGo

它的膠片鏈接 https://speakerdeck.com/mgechev/angular-toolset-support?slide=69

Angular模板編譯器收到一個(gè)組件和它的上下文(可以這認(rèn)為是組件在組件樹(shù)上的位置)作為輸入筹燕,并產(chǎn)生了如下文件:

  • *.ngfactory.ts 我們?cè)谡f(shuō)明組件上下文的小節(jié)會(huì)仔細(xì)看看這些文件
  • *.css.shim.ts 樣式作用范圍被隔離后的css文件,根據(jù)組件所設(shè)置的 ViewEncapsulation 模式不同而會(huì)有不同
  • *.metadata.json 當(dāng)前組件/模塊的裝飾器元數(shù)據(jù)信息衅鹿,這些數(shù)據(jù)可以被想象成以json格式傳遞給 @Component @NgModule 裝飾器的信息撒踪。

* 是一個(gè)文件名占位符,例如對(duì)于hero.component.ts這樣的組件大渤,編譯器生成的文件是 hero.component.ngfactory.ts, hero.component.css.shim.tshero.component.metadata.json制妄。*.css.shim.ts和我們討論的主題關(guān)系不大,因此不會(huì)對(duì)它詳細(xì)描述泵三。如果你希望多了解 *.metadata.json 文件耕捞,你可以看看“AoT和第三方模塊”小節(jié)。

*.ngfactory.ts 的內(nèi)部結(jié)構(gòu)

它包含了如下的定義:

  • _View_{COMPONENT}_Host{COUNTER} 我們稱之為internal host component
  • _View_{COMPONENT}{COUNTER} 我們稱之為 internal component

以及下面兩個(gè)函數(shù)

  • viewFactory_{COMPONENT}_Host{COUNTER}
  • viewFactory_{COMPONENT}{COUNTER}

其中的 {COMPONENT} 是組件的控制器名字切黔,而 {COUNTER} 是一個(gè)無(wú)符號(hào)整數(shù)砸脊。他們都繼承了 AppView,并且實(shí)現(xiàn)了下面的方法:

  • createInternal 組件的渲染器
  • destroyInternal 執(zhí)行事件監(jiān)聽(tīng)器等的清理
  • detectChangesInternal 以內(nèi)聯(lián)緩存優(yōu)化后的邏輯執(zhí)行變化檢測(cè)

上述這些工廠函數(shù)只在生成的AppView實(shí)例中才存在纬霞。

我前面說(shuō)過(guò)凌埂,detectChangesInternal中的代碼是JS虛擬機(jī)友好的。

<div>{{newName}}</div>
<input type="text" [(ngModel)]="newName">

我們來(lái)看看編譯后這個(gè)模板的代碼诗芜,detectChangesInternal方法的代碼看起來(lái)像是這樣的:

// ...
var currVal_6 = this.context.newName;
if (import4.checkBinding(throwOnChange, this._expr_6, currVal_6)) {
    this._NgModel_5_5.model = currVal_6;
    if ((changes === null)) {
        (changes = {});
    }
    changes['model'] = new import7.SimpleChange(this._expr_6, currVal_6);
    this._expr_6 = currVal_6;
}
this.detectContentChildrenChanges(throwOnChange);
// ...

假設(shè)currVal_6的值是3瞳抓,this_expr_6的值是1埃疫,我們來(lái)跟蹤看看這個(gè)方法的執(zhí)行。對(duì)于這樣的一個(gè)調(diào)用 import4.checkBinding(1, 3)孩哑,在生產(chǎn)環(huán)境下栓霜,checkBinding 執(zhí)行的是下面的檢查:

1 === 3 || typeof 1 === 'number' && typeof 3 === 'number' && isNaN(1) && isNaN(3);

上述表達(dá)式返回false,因此我們將把變化保持下來(lái)横蜒,以及直接更新 NgModel 的屬性 model 的值胳蛮,在這之后,detectContentChildrenChanges 方法會(huì)被調(diào)用丛晌,它將為整個(gè)模板內(nèi)容的子級(jí)調(diào)用 detectChangesInternal仅炊。一旦 NgModel 指令發(fā)現(xiàn)了 model 屬性發(fā)生了變化,它就會(huì)(幾乎)直接調(diào)用渲染器來(lái)更新對(duì)應(yīng)的DOM元素澎蛛。

目前為止抚垄,我們還沒(méi)有碰到任何特殊的,或者特別復(fù)雜的邏輯谋逻。

context 屬性

也許你已經(jīng)注意到了在internal component內(nèi)部訪問(wèn)了 this.context 屬性呆馁。

譯者:internal component 指的前一小節(jié)的 _View_{COMPONENT}{COUNTER} 函數(shù)

internal component中的 context 是這個(gè)組件的控制器的實(shí)例,例如這樣的一個(gè)組件:

@Component({
  selector: 'hero-app',
  template: '<h1>{{ hero.name }}</h1>'
})
class HeroComponent {
  hero: Hero;
}

this.context 就是 new HeroComponent()毁兆,這意味著如果在 detectChangesInternal 中我們需要訪問(wèn) this.context.name 的話浙滤,就帶來(lái)了一個(gè)問(wèn)題:如果我們使用AoT模式編譯組件的模板,由于這個(gè)模式會(huì)生成TypeScript代碼荧恍,因此我們要確保在組件的模板中只訪問(wèn) this.context 中的public成員瓷叫。這是為何?由于TypeScript的類屬性有訪問(wèn)控制送巡,強(qiáng)制類外部只能訪問(wèn)類(及其父類)中的public成員,因此在internal component內(nèi)部我們無(wú)法訪問(wèn) this.context 的任何私有成員盒卸。因此骗爆,下面這個(gè)組件:

@Component({
  selector: 'hero-app',
  template: '<h1>{{ hero.name }}</h1>'
})
class HeroComponent {
  private hero: Hero;
}

以及這個(gè)組件

class Hero {
  private name: string;
}

@Component({
  selector: 'hero-app',
  template: '<h1>{{ hero.name }}</h1>'
})
class HeroComponent {
  hero: Hero;
}

在生成出來(lái)的 *.ngfactory.ts 中,都會(huì)拋出編譯錯(cuò)誤蔽介。第一個(gè)組件代碼摘投,internal component無(wú)法訪問(wèn)到在 HeroComponent 類中被聲明為 private 的 hero 屬性。第二個(gè)組件代碼中虹蓄,internal component無(wú)法訪問(wèn)到 hero.name 屬性犀呼,因?yàn)樗?Hero 類中被聲明為private。

AoT與封裝

好吧薇组,我們只能在組件模板中綁定public屬性外臂,以及調(diào)用public方法。但是律胀,如何確保組件的封裝性宋光?在開(kāi)始的時(shí)候貌矿,這可能不是一個(gè)大問(wèn)題,但是想象一下下面這個(gè)場(chǎng)景:

// component.ts
@Component({
  selector: 'third-party',
  template: `
    {{ _initials }}
  `
})
class ThirdPartyComponent {
  private _initials: string;
  private _name: string;

  @Input()
  set name(name: string) {
    if (name) {
      this._initials = name.split(' ').map(n => n[0]).join('. ') + '.';
      this._name = name;
    }
  }
}

這個(gè)組件有一個(gè)屬性 name罪佳,它只能寫(xiě)入而無(wú)法讀取逛漫。在 name 屬性的 setter 方法中,計(jì)算了 _initials 屬性的值赘艳。

我們可以用類似下面的方式使用這個(gè)組件:

@Component({
  template: '<third-party [name]="name"></third-party>'
  // ...
})
// ...

在JiT編譯模式下酌毡,一切正常,因?yàn)镴iT模式只生成JavaScript代碼蕾管。每次 name 屬性的值發(fā)生變化枷踏,_initials 就會(huì)被重新計(jì)算。但是娇掏,這個(gè)組件卻不是AoT友好的呕寝,必須改為:

// component.ts
@Component({
  selector: 'third-party',
  template: `
    {{ initials }}
  `
})
class ThirdPartyComponent {
  initials: string;
  private _name: string;

  @Input()
  set name(name: string) {...}
}

codelyzer 這個(gè)工具可以確保你在模板中每次都能訪問(wèn)到public成員。

這讓組件的使用者可以這樣做:

import {ThirdPartyComponent} from 'third-party-lib';

@Component({
  template: '<third-party [name]="name"></third-party>'
  // ...
})
class Consumer {
  @ViewChild(ThirdPartyComponent) cmp: ThirdPartyComponent;
  name = 'Foo Bar';

  ngAfterViewInit() {
    this.cmp.initials = 'M. D.';
  }
}

對(duì)public屬性 initials 的直接修改導(dǎo)致了組件處于不一致性的狀態(tài):組件的 _name 的值是 Foo Bar婴梧,但是它的 initials 的值是 M. D.下梢,而非 F. B.

在Angular的源碼中塞蹭,我們可以找到解決的辦法孽江,使用TypeScript的 /** @internal */ 注釋聲明,就能夠達(dá)到既保證組件代碼對(duì)AoT友好番电,又能夠確保組件的封裝良好的目的岗屏。

// component.ts
@Component({
  selector: 'third-party',
  template: `
    {{ initials }}
  `
})
class ThirdPartyComponent {
  /** @internal */
  initials: string;
  private _name: string;

  @Input()
  set name(name: string) {...}
}

initials 屬性仍然是public的。我們?cè)谑褂?tsc 編譯這個(gè)組件時(shí)漱办,設(shè)置 --stripInternal--declarations參數(shù)这刷,initials 屬性就會(huì)從組件的類型定義文件(即 .d.ts 文件)中被刪掉。這樣我們就可以做到在我們的類庫(kù)內(nèi)部使用它娩井,但是我們的組件使用者無(wú)法使用它暇屋。

ngfactory.ts 概要

我們來(lái)對(duì)幕后所發(fā)生的一切做一些概要描述。拿我們前面的例子中的 HeroComponent 為例洞辣,Angular編譯器會(huì)生成兩個(gè)類:

  • _View_HeroComponent_Host1 這是 internal host component
  • _View_HeroComponent1 這是 internal component

_View_HeroComponent1 負(fù)責(zé)渲染這個(gè)組件的模板咐刨,以及進(jìn)行變化檢測(cè)。在執(zhí)行變化檢測(cè)時(shí)扬霜,它會(huì)對(duì) this.context.hero.name之前保存的值和當(dāng)前值做比較定鸟,一旦發(fā)現(xiàn)這兩個(gè)值不一致,<h1/>元素就會(huì)被更新著瓶,這意味著我們必須保持 this.context.herohero.name 是public的联予。這一點(diǎn)可以通過(guò) codelyzer 這個(gè)工具來(lái)輔助確保。

另外,_View_HeroComponent_Host1 則負(fù)責(zé) <hero-app></hero-app>_View_HeroComponent1 本身的渲染躯泰。

這個(gè)例子可以以下面這個(gè)圖來(lái)總結(jié):

AoT vs JiT 開(kāi)發(fā)體驗(yàn)

這個(gè)小結(jié)谭羔,我們來(lái)討論使用AoT開(kāi)發(fā)和JiT開(kāi)發(fā)的另一種體驗(yàn)。

可能使用JiT對(duì)開(kāi)發(fā)體驗(yàn)的沖擊最大的就是JiT模式為internal componentinternal host component生成的是JavaScript代碼麦向,這意味著組件的控制器中的屬性都是public的瘟裸,因此我們不會(huì)得到任何編譯錯(cuò)誤。

在JiT模式下诵竭,一旦我們啟動(dòng)了應(yīng)用话告,根組件的根注入器和所有的指令就已經(jīng)準(zhǔn)備就緒了(他們被包含在 BrowserModule 和其他所有我們?cè)诟K中引入的模塊中了)。元數(shù)據(jù)信息會(huì)傳遞給編譯器卵慰,用于對(duì)根組件的模板的編譯沙郭。一旦編譯器生成了JiT下的代碼,編譯器就擁有了用于生成各個(gè)子組件的所有元數(shù)據(jù)信息裳朋。由于編譯器此時(shí)不僅知道了當(dāng)前層級(jí)的組件有那些provider可用病线,還可以知道那些指令是可見(jiàn)的,因此它可以給所有的組件生成代碼鲤嫡。

這一點(diǎn)讓編譯器在訪問(wèn)了模板中的一個(gè)元素時(shí)送挑,知道該怎么工作。根據(jù)是否有選擇器是 bar-baz 的指令/組件暖眼,<bar-baz></bar-baz> 這樣的一個(gè)元素就有了兩種不同的解釋惕耕。編譯器在創(chuàng)建了 <bar-baz></bar-baz> 這樣的一個(gè)元素的同時(shí),是否還同時(shí)初始化 bar-baz 對(duì)應(yīng)的組件類的實(shí)例诫肠,則完全取決于當(dāng)前階段的編譯過(guò)程的元數(shù)據(jù)信息司澎。

這里有一個(gè)問(wèn)題,在編譯階段栋豫,我們?nèi)绾沃乐噶钤谡麄€(gè)組件樹(shù)上是否可訪問(wèn)挤安?得益于Angular框架的良好設(shè)計(jì),我們通過(guò)靜態(tài)代碼分析就可以做到丧鸯。Chuck Jazdzewski 和 Alex Eagle 在這個(gè)方向上做出了令人驚嘆的成果漱受,他們實(shí)現(xiàn)了 MetadataCollector 和相關(guān)的模塊。MetadataCollector 所做的事情就是通過(guò)遍歷組件樹(shù)來(lái)獲取每個(gè)組件和NgModule的元數(shù)據(jù)信息骡送,這個(gè)過(guò)程中,很多牛逼的技術(shù)被用到絮记,可惜這些技術(shù)超出了本文的范疇摔踱。

AoT與第三方模塊

為了編譯組件的模板,編譯器需要組件的元數(shù)據(jù)信息怨愤,我們來(lái)假設(shè)我們的應(yīng)用使用到了一些第三方組件派敷,Angular的AoT編譯器是如何獲取這些已經(jīng)被編譯成JavaScript代碼的組件的元數(shù)據(jù)信息的?這些組件庫(kù)必須連帶發(fā)布對(duì)應(yīng)的 *.metadata.json 文件,這樣才能夠?qū)σ粋€(gè)引用了它的頁(yè)面進(jìn)行AoT編譯篮愉。

如果你想了解如何使用Angular編譯器腐芍,例如如何編譯你自定義庫(kù)使得他們能夠被用于以AoT編譯的應(yīng)用,那請(qǐng)?jiān)L問(wèn)這個(gè)鏈接 https://github.com/angular/mobile-toolkit/blob/master/app-shell/gulpfile.ts#L52-L54

我們從AoT中獲得了什么试躏?

你可能已經(jīng)想到了猪勇,AoT給我們帶來(lái)了性能的提升。以AoT方式開(kāi)發(fā)的Angular應(yīng)用的初次渲染性能要比JiT的高的多颠蕴,這是由于JS虛擬機(jī)需要的計(jì)算量大大減少了泣刹。我們只在開(kāi)發(fā)過(guò)沖中,將組件的模板編譯成js代碼犀被,在此之后椅您,用戶不需要等待再次編譯。

下面這個(gè)圖中寡键,可以看出JiT渲染方式在初始化過(guò)程中所消耗的時(shí)間:

下面這個(gè)圖你可以看出AoT方式初始化在初始化過(guò)程中所消耗的時(shí)間:

Angular編譯器不僅能夠生產(chǎn)JavaScript掀泳,還能夠生成TypeScript,這一點(diǎn)還帶給我們要給非常棒的特性:在模板中進(jìn)行類型檢查西轩。

由于應(yīng)用的模板是純JavaScript/TypeScript阀溶,我們可以精確的知道哪些東西在哪被用到了萌壳,這一點(diǎn)讓我們可以對(duì)代碼進(jìn)行有效的搖樹(shù)操作,它能夠把所有的未使用過(guò)的指令/模塊從我們生產(chǎn)環(huán)境中的應(yīng)用代碼包中給刪除掉。這首要的一點(diǎn)是拙友,我們的應(yīng)用程序代碼包中,再也無(wú)需包含 @angular/compiler 這個(gè)模塊庇楞,因?yàn)槲覀冊(cè)趹?yīng)用的運(yùn)行時(shí)根本就用不到它钦无。

有一點(diǎn)需要注意的是,一個(gè)中大型的應(yīng)用代碼包祠汇,在進(jìn)行AoT編譯過(guò)之后仍秤,可能會(huì)比使用JiT方式編譯的代碼包要大一些。這是因?yàn)?ngc 生成的對(duì)JS虛擬機(jī)友好的代碼比基于HTML模板的代碼要冗長(zhǎng)一些可很,并且這些代碼還包含了臟檢查邏輯诗力。如果你想降低你的應(yīng)用代碼的尺寸,你可以通過(guò)懶加載的方式來(lái)實(shí)現(xiàn)我抠,Angular內(nèi)建的路由已經(jīng)支持這一點(diǎn)了苇本。

在某些場(chǎng)合,JiT模式的編譯根本就無(wú)法進(jìn)行菜拓,這是由于JiT在瀏覽器中瓣窄,不僅生成代碼,它還使用 eval 來(lái)運(yùn)行它們纳鼎,瀏覽器的內(nèi)容安全策略以及特定的環(huán)境不允許這些被生成的代碼被動(dòng)態(tài)的運(yùn)行俺夕。

最后一個(gè)但不是唯一的:節(jié)能裳凸!在接受到的是編譯后的代碼時(shí),用戶的設(shè)備可以花更少的時(shí)間運(yùn)行他們劝贸,這節(jié)約了電池的電力姨谷。節(jié)能的量有多少呢?下面是我做的一些有趣的計(jì)算的結(jié)果:

基于《Who Killed My Battery: Analyzing Mobile Browser Energy Consumption》這偏文章的結(jié)論映九,訪問(wèn)Wikipedia時(shí)梦湘,下載和解析jQuery的過(guò)程大約需要消耗4焦耳的能量。這個(gè)文章沒(méi)有提及所使用的jQuery的確切版本氯迂,基于文章發(fā)表的日期践叠,我估計(jì)版本號(hào)是1.8.x。Wikipedia采用了gzip對(duì)靜態(tài)資源做壓縮嚼蚀,這意味著jQuery1.8.3的尺寸約33k禁灼。而被最小化并且gzip壓縮后的 @angular/compiler 包的尺寸在103k,這意味著對(duì)這些代碼的下載和解析需要消耗12.5焦的能量(我們可以忽略JiT的運(yùn)算還會(huì)增加能耗的事實(shí)轿曙,這是因?yàn)閖Query和@angular/compiler這兩個(gè)場(chǎng)景弄捕,都是使用了要給單一的TCP鏈接,這從是最大的能耗所在)导帝。

iPhone6s的電池容量是6.9Wh守谓,即24840焦∧ィ基于AngularJs1.x官網(wǎng)的月訪問(wèn)量可以得知現(xiàn)在大約有一百萬(wàn)名Angular開(kāi)發(fā)者斋荞,平均每位開(kāi)發(fā)者構(gòu)建了5個(gè)應(yīng)用,每個(gè)應(yīng)用每天約100名用戶虐秦。5個(gè)app * 1m * 100用戶 = 500m平酿,在使用JiT編譯這些應(yīng)用,他們就需要下載 @angular/compiler 包悦陋,這將給地球帶來(lái) 500m * 12.5J = 6250000000J焦(=1736.111111111千瓦時(shí))的能量消耗蜈彼。根據(jù)Google搜索結(jié)果,1千瓦時(shí)約等于12美分俺驶,這意味著我們每天需要消耗約 210 美元幸逆。注意到我們還沒(méi)進(jìn)一步對(duì)代碼做搖樹(shù)操作,這可能會(huì)讓我們的應(yīng)用程序代碼降低至少一半暮现!

結(jié)論

Angular的編譯器利用了JS虛擬機(jī)的內(nèi)聯(lián)緩存機(jī)制还绘,極大的提升了我們的應(yīng)用程序的性能。首要的一點(diǎn)是我們把編譯過(guò)程作為構(gòu)建應(yīng)用的一個(gè)環(huán)節(jié)栖袋,這不僅解決了非法eval的問(wèn)題蚕甥,還允許我們對(duì)代碼做高效的搖樹(shù),降低了首次渲染的時(shí)間栋荸。

不在運(yùn)行時(shí)編譯是否讓我們失去是什么嗎?在一些非常極端的場(chǎng)景下,我們會(huì)按需生成組件的模板晌块,這就需要我們價(jià)值一個(gè)未編譯的組件爱沟,并在瀏覽器中執(zhí)行編譯的過(guò)程,在這樣的場(chǎng)景下匆背,我們就需要在我們的應(yīng)用代碼包中包含 @angular/compiler 模塊呼伸。AoT編譯的另一個(gè)潛在缺點(diǎn)是,它會(huì)造成中大型應(yīng)用的代碼包尺寸變大钝尸。由于生成的組件模板的JavaScript代碼比組件模板本身的尺寸更大括享,這就可能造成最終代碼包的尺寸更大一些。

總的來(lái)說(shuō)珍促,AoT編譯是一個(gè)很好的技術(shù)铃辖,現(xiàn)在它已經(jīng)被集成到了Angular-seed和angular-cli中,所以猪叙,你今天就可以去使用它了娇斩。

參考資料

題外話

這些文章都是我們?cè)谘邪l(fā)Jigsaw七巧板過(guò)程中的技術(shù)總結(jié),如果你喜歡這個(gè)文章穴翩,請(qǐng)幫忙到 Jigsaw七巧板的工程上點(diǎn)個(gè)星星鼓勵(lì)我們一下(點(diǎn)擊閱讀原文看直達(dá) https://github.com/rdkmaster/jigsaw)犬第,這樣我們會(huì)更有動(dòng)力寫(xiě)出類似高質(zhì)量的文章。Jigsaw七巧板現(xiàn)在處于起步階段芒帕,非常需要各位的呵護(hù)歉嗓。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市背蟆,隨后出現(xiàn)的幾起案子鉴分,更是在濱河造成了極大的恐慌,老刑警劉巖淆储,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冠场,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡本砰,警方通過(guò)查閱死者的電腦和手機(jī)碴裙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)点额,“玉大人舔株,你說(shuō)我怎么就攤上這事』估猓” “怎么了载慈?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)珍手。 經(jīng)常有香客問(wèn)我办铡,道長(zhǎng)辞做,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任寡具,我火速辦了婚禮秤茅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘童叠。我一直安慰自己框喳,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布厦坛。 她就那樣靜靜地躺著五垮,像睡著了一般。 火紅的嫁衣襯著肌膚如雪杜秸。 梳的紋絲不亂的頭發(fā)上放仗,一...
    開(kāi)封第一講書(shū)人閱讀 51,155評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音亩歹,去河邊找鬼匙监。 笑死,一個(gè)胖子當(dāng)著我的面吹牛小作,可吹牛的內(nèi)容都是我干的亭姥。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼顾稀,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼达罗!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起静秆,我...
    開(kāi)封第一講書(shū)人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤粮揉,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后抚笔,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體扶认,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年殊橙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了辐宾。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡膨蛮,死狀恐怖叠纹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情敞葛,我是刑警寧澤誉察,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站惹谐,受9級(jí)特大地震影響持偏,放射性物質(zhì)發(fā)生泄漏驼卖。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一综液、第九天 我趴在偏房一處隱蔽的房頂上張望款慨。 院中可真熱鬧,春花似錦谬莹、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至井誉,卻和暖如春蕉扮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背颗圣。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工喳钟, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人在岂。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓奔则,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親蔽午。 傳聞我的和親對(duì)象是個(gè)殘疾皇子易茬,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • core package 概要:Core是所有其他包的基礎(chǔ)包.它提供了大部分功能包括metadata,templa...
    LOVE小狼閱讀 2,576評(píng)論 0 3
  • 史上最簡(jiǎn)單Angular2教程及老,大叔都學(xué)會(huì)了 作者:王芃 wpcfan@gmail.com 第一節(jié):初識(shí)Angul...
    接灰的電子產(chǎn)品閱讀 58,771評(píng)論 76 248
  • 為什么需要編譯 Angular應(yīng)用中包含的組件抽莱、HTML模板(比如:@Directive、@Component骄恶、@...
    OnePiece索隆閱讀 2,931評(píng)論 3 4
  • 感恩安拉食铐,感恩萬(wàn)物,感恩一切的恩賜僧鲁。感恩一切的流動(dòng)虐呻。 感恩每一位老師對(duì)我們學(xué)校工作的付出。 感恩小馬對(duì)我的這份支持...
    黛兒微笑閱讀 214評(píng)論 0 0
  • 彩霞舞不若舞女悔捶, 君生死與其共進(jìn)铃慷。 江山不過(guò)白發(fā)翁, 惺惺相惜天涯路蜕该。
    薄荷菇?jīng)鲂奈鰅閱讀 221評(píng)論 0 0