[翻译]Angular中的AoT(Ahead-of-Time Compilation in Angular )

此文是angular-seed的作者的文,发布于2016/08/14,原文地址
(angular-seed最后release时间为2017/03/25)


最近我在angular-seed中增加支持了 Ahead-of-Time(AoT) compilation ,收到了关于此特性的很多疑问。以下7点将能够解答绝大多数问题,下文将逐一展开:

  • 为什么Angular中需要编译?(Why we need compilation in Angular?)
  • 什么内容需要进行编译?(What needs to be compiled?)
  • 编译如何进行?(How it gets compiled?)
  • Just-in-Time (JiT) vs Ahead-of-Time (AoT) 分别在何时进行?(When the compilation takes place? Just-in-Time (JiT) vs Ahead-of-Time (AoT).)
  • AoT后会生成什么?(What we get from AoT?)
  • AoT编译如何工作的?(How the AoT compilation works?)
  • JiT与AoT相比,有什么缺点?(Do we loose anything from using AoT vs JiT?)

为什么Angular中需要编译?(Why we need compilation in Angular?)

简而言之,编译能使Angular的应用有更高的效率。我所说的效率指的是性能的提高,但这也会加大能源、有时还有带宽的消耗。
AngulzarJS 1.x 在页面渲染和变化检测的实现采用了非常动态的方式。例如,AngulzarJS 1.x的编译器是非常通用的,它通过进行一系列动态计算 以期可以 编译任何模板文件。尽管大多数情况它能够正常工作,但因为他们的动态特性 使JavaScript Virtual Machines(VM)对这些计算 的优化程度较低(the JavaScript Virtual Machines (VM) struggles with optimizing the calculations on lower level)。
因为VM不知道scope对象的结构,scope对象提供脏检查逻辑的上下文环境 ,VM的内联缓存会出现很多misses ,这降低执行速度(Since the VM doesn’t know the shapes of the objects which provide context for the dirty-checking logic (i.e. the so called scope), it’s inline caches get a lot of misses which slows the execution down.)
Angulzar 2及以上的版本,执行变化检测和渲染方式与1不同。不再 对每个独立的component的渲染和和变化检测 使用同样的逻辑,而是在执行时或编译时 框架会生成VM友好的代码 ,这能使JVM 使用属性访问的方式访问缓存、更快地执行变化检测/渲染逻辑(This allows the JavaScript virtual machine to perform property access caching and execute the change detection/rendering logic much faster.)。
例如:以下代码截取自我的AngularJs1.x的轻量级实现,代码中我们使用深度优先遍历了整个scope树,查找我们绑定的数据的变化。这种实现方式对任何directive都适用。然而,与代码二 directive特定的代码相比 此实现明显更慢一些。

// ... 代码一
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();
  }
};
// ... 代码二
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);

代码二截取了 angular-seed项目中,编译后的组件生成的 detectChangesInternal 方法的部分实现(The snippet above contains a piece of the implementation of the generated detectChangesInternal method of a compiled component from the angular-seed project.)。它直接使用属性访问的方式获取绑定数据,并使用最高效的方式比较新值和原值。一旦发现二者不同,它只更新绑定值影响的DOM元素。

什么内容需要进行编译?(What needs to be compiled?)

我们在回答为何需要编译这个问题的同时,也回答了什么内容需要编译。 我们希望将模板文件编译成JavaScript的类。这些类中的方法包含检测绑定数据变化和渲染用户界面的逻辑 。这样我们就不需要底层平台耦合(除了markup的格式)。换言之,通过实现各种不同的渲染器, 我们可以 使用相同的 AoT编译的组件并渲染它,而无需改变代码。例如,只要渲染器能够理解传参,该组件就可以在NativeScript中被渲染(In other words, by having a different implementation of the renderer we can use the same AoT compiled component and render it without any changes in the code. So the component above could be rendered in NativeScript, for instance, as soon as the renderer understands the passed arguments.)

编译如何进行(How it gets compiled?)

Just-in-Time (JiT) 和 Ahead-of-Time (AoT) 分别在何时进行?(When the compilation takes place? Just-in-Time (JiT) vs Ahead-of-Time (AoT).)

Angular的编译器优点是在runtime(例如用户的浏览器)或build-time(作为build流程中的一步)都可以调用它。这是由于Angular的可移植性,我们可以在任何 有javascript vm的平台运行它,所以让Angular在浏览器和node中都可以被编译。
Just-in-Time编译过程的事件流Flow of events with Just-in-Time Compilation

  • 我们先来看典型的没有AoT的development流程
    • 用TypeScript开发Angular应用 (Development of Angular application with TypeScript.)
    • 使用tsc编译应用(ompilation of the application with tsc.)
    • 打包(Bundling)
    • 压缩(Minification)
    • 部署(Deployment)
  • 我们了部署该应用, 用户打开他们的浏览器,会经历以下几步(不包含严格内容安全策略 without strict CSP)
    • 下载所有的js资源 (Download all the JavaScript assets)
    • 执行Angular 引导逻辑(Angular bootstraps.)
    • Angular进入JiT编译流程,如将应用中的每个组件生成js(Angular goes through the JiT compilation process, i.e. generation of JavaScript for each component in our application)
    • 渲染该应用(The application gets rendered)

Ahead-of-Time编译过程的事件流Flow of events with Just-in-Time Compilation

  • 有AoT的development流程

    • 用TypeScript开发Angular应用 (Development of Angular application with TypeScript.)
    • 使用ngc编译应用(Compilation of the application with ngc.)
      • 使用Angular的编译器编译模板文件,通常生成TypeScript(Performs compilation of the templates with the Angular compiler and generates (usually) TypeScript
      • 将TypeScript编译为JavaScript
    • 打包(Bundling)
    • 压缩(Minification)
    • 部署(Deployment)
  • 虽然上述AoT流程看起来比JiT的流程稍复杂一些,但用户在浏览器端的流程只要如下几步:

    • 下载所有的静态资源 (Download all the assets)
    • 执行Angular 引导逻辑(Angular bootstraps.)
    • 渲染该应用(The application gets rendered)

我们可以看到,JiT流程中,用户在浏览器端打开页面的步骤第三步,在AoT流程中不见了。这会带来更好更快的用户体验,在诸如angular-seed和angular-cli这种工具,能够显著地自动化构建(on top of that tools like angular-seed and angular-cli will automate the build process dramatically.)

总结,对Angular来说 JiT和AoT主要的不同点在于

  1. 编译发生的时间不同
  2. JiT生成JavaScript(由于代码是在浏览器端被编译为JavaScript,生成TypeScript没什么意义 (TypeScript doesn’t make a lot of sense since the code needs to be compiled to JavaScript in the browser)),而AoT通常生成TypeScript

AoT后会生成什么?(What we get from AoT?)

AoT编译如何工作的?(How the AoT compilation works?)

深入理解Ahead-of-Time Compilation (Ahead-of-Time Compilation in Depth)

这个小节回答了以下三个问题:

  • AoT编译生成了什么?(What artifacts the AoT compiler produces?)
  • 生成的文件的执行环境是什么?(What the context of the produced artifacts is?)
  • 如何开发兼顾AoT友好和良好封装的代码?(How to develop both: AoT friendly and well encapsulated code?)

编译的过程我们快速过一下即可,没必要详细解释完整的@angular/compiler的代码,如果你对分析、解析、代码生成有兴趣,可以看一下关于“The Angular 2 Compiler” by Tobias Bosch的讨论或者slide deck.

Angular模板编译器获取 一个组件和和上下文作为输入(The Angular template compiler receives as an input a component and a context (we can think of the context as a position in the component tree)),生成如下文件:

  • *.ngfactory.ts - 下一节我们将看一下这个文件
  • *.css.shim.ts - 基于组件的ViewEncapsulation模式的 scoped css文件。对本文讨论的主题关系不大所以就不详细描述了。
  • *.metadata.json - 与当前组件(或NgModule)有关的元数据。我们可以将其看做我们传给@Component, @NgModule装饰器的JSON对象。将在 ‘AoT and third-party modules’这一节看一下这个文件
    (*是文件名的占位符)

*.ngfactory.ts
包含以下定义:

  • _View_{COMPONENT}_Host{COUNTER} - 我们称之为“内部主组件”(internal host component)
  • _View_{COMPONENT}{COUNTER} - 我们称之为“内部组件”(internal component)
    和两个函数
  • viewFactory_{COMPONENT}_Host{COUNTER}
  • viewFactory_{COMPONENT}{COUNTER}
    以上{COMPONENT}是组件的controller的名字,{COUNTER}是无符号整数
    两个类都继承了AppView,实现了以下方法:
  • createInternal - 渲染组件
  • destroyInternal - 执行清除工作(移除事件监听等)
  • detectChangesInternal - 使用内联缓存优化过的方法进行变化检测
    以上的factory方法只适用于生成的AppViews的实例(The factory functions above are only responsible for instantiation of the generated AppViews)
    像上文提到的,detectChangesInternal 的代码VM友好。我们来看一下模板的编译版本:

AoT vs JiT 开发经验
在此小节中我们会讨论使用AoT和 JiT开发体验的另一个不同点。
最大的不同点应该是Jit 模式时,internal component 和 the internal host component会被定义为JavaScript。这表示组件的controller中的字段始终是public访问权限,我们能访问任何private的字段而得不到任何编译时错误。
在JiT一旦我们开始了应用的引导程序,我们就已经能够在root componnet中访问到 root injector和所有指令 (他们包含在BrowserModule和我们在root module中引进的所有其他的module)。元数据将会在root component的视图文件的编译过程中被传给编译器,一旦编译器在JiT模式下生成了root component的代码,编译器就有了所有的元数据,这些数据也会用于生成所有子组件的代码(This metadata will be passed to the compiler for the process of compilation of the template of the root component. Once the compiler generates the code with JiT, it has all the metadata which should be used for the generation of the code for all child components)。编译器不仅已经知道在组件树的级别可访问哪些provider还知道使用了哪些指令,所以编译器能够给所有的组件生成代码(It can generate the code for all of them since it already knows not only which providers are available at this level of the component tree but also which directives are visible there)。
当 在模板中访问某元素时 ,编译器能够知道要做什么。例如组件<bar-baz></bar-baz>

AoT 和第三方模块
既然编译器需要组件的元数据以编译他们 的模板文件,我们可以摄像 在我们的应用中我们使用了第三方组件库。Angular AoT编译器如何能知道这些组件定义的元数据,因为这些组件是普通的JavaScript?编译器不能知道!所以引用了Angular library之外的第三方库,要用AoT模式编译应用,第三方库需要分布在 编译器生成的 *.metadata.json

AoT有什么优点?(What we get from AoT?)

你可能会想,AoT带来良好的性能。初始化渲染的性能AoT模式比JiT模式快得多,因为AoT大大降低了JVM要做的计算量。我们只需要在开发过程中将模板文件编译为JavaScript文件 一次。

JiT与AoT相比,有什么缺点?(Do we loose anything from using AoT vs JiT?)

总结

Angular编译器利用JVM的行内缓存机制,显著地改善了我们的应用的性能。另外,我们在构建过程中进行编译, 解决了禁用eval的问题,能够让我们进行更高效的tree-shaking,降低初始渲染时间。

不在运行时进行编译是否有缺点?在个别情况下,我们 可能会继续生产组件的模板文件,这就需要我们载入未编译的组件,然后在浏览器执行编译过程,在这种情况下我们需要在应用中引入@angular/compiler模块

AoT还有一个潜在的缺点是,在中大型应用中,AoT打包后包的大小会增加。因为组件的模板文件生成JavaScript代码量比模板文件本身要大,这很有可能导致最终打包的大体积。

总之,AoT编译是一种很好的技术,它已经被angular-seed和angular-cli集成了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 前两天,Jigsaw七巧板上来了个issue https://github.com/rdkmaster/jigsa...
    阿踏阅读 3,187评论 0 5
  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi阅读 7,102评论 0 10
  • TL;DR: In the first part of the Glossary of Modern JS Con...
    Transnet2014阅读 499评论 0 0
  • 为什么需要编译 Angular应用中包含的组件、HTML模板(比如:@Directive、@Component、@...
    OnePiece索隆阅读 2,869评论 3 4
  • 无论什么时候,做任何事,都要有个好身体,身体健康才是根本, 锻炼身体,让自己有个健康的体魄,爱身边的每个人,珍惜每...
    Lzr_2017阅读 58评论 0 0