前兩天冠王,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.ts
和 hero.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.hero
和 hero.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 component和internal 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
中,所以猪叙,你今天就可以去使用它了娇斩。
參考資料
- 內(nèi)聯(lián)緩存 http://mrale.ph/blog/2012/06/03/explaining-js-vms-in-js-inline-caches.html
- 2.5X Smaller Angular 2 Applications with Google Closure Compiler http://blog.mgechev.com/2016/07/21/even-smaller-angular2-applications-closure-tree-shaking/
- Who Killed My Battery: Analyzing Mobile Browser Energy Consumption https://crypto.stanford.edu/~dabo/pubs/abstracts/browserpower.html
- Angular的源碼 https://github.com/angular/angular
題外話
這些文章都是我們?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ù)歉嗓。