此文是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主要的不同点在于
:
-
编译发生的时间不同
; -
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)。
当 在模板中访问某元素时 ,编译器能够知道要做什么。例如组件
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集成了。