Angular 2 中的编译器与预编译(AoT)优化

Compilation in Angular

源代码浏览器中可运行的程序之间的过程都可以被认为是Compile过程,在Angular程序中,源代码中可能包含@Directive、@Component、@NgModule、@Pipe等各种内容,无论是TypeScript的Annotation还是Template中的double binding,这些最后都会变成可被浏览器解析的语言运行起来。

我们可以将整个compile过程简化为:

Inputs(源代码)-----Parser(解析器)----->Instantiate(实例化)

在后面的文章中我们逐步来介绍这三部分在Angular中具体的工作和原理

Inputs(源代码)

由于篇幅有限,我们以Component和Directive的组合为例来进行介绍

hello.component.ts

@Component({ templateUrl:'hello_comp.html' }) class HelloComp{ user = {name:'Tobias'}; } 

hello_comp.html

<form> <div>Hello {{user.name}}</div> <input ngModel> </form> 

首先在HelloComp中定义了user的初始化值,并在template中渲染出来,在template中还包含了ngModel绑定的input。

我们将Directive代码也添加进来,Directive的selector支持css选择器,当在template代码中发现符合css选择器中的element时,就会实例化相应Directive。

@Directive({selector:'form'}) class NgForm{...} @Directive({selector:'[ngModel]'}) class NgModel{ ... constructor(form:NgForm){...} } 

以上的简化代码也很容易理解,form和[ngModel]的selector分别在<form>标签和带有ngModel的attribute标签中生成了对应的Directive实例。值得一提的是在NgModel的directive中依赖了NgForm,这意味着NgModel的实例将在template的父元素中查找form依赖,直到命中为止。

以上我们已经明确了原始代码的所有功能,这些被定义Component和Directive正是我们compiler的Inputs,下面就来介绍Compiler对代码的Parse过程

Parser(解析器)

再关注一下之前的hello_comp.html

<form> <div>Hello {{user.name}}</div> <input ngModel> </form> 

被parser翻译后的template应该对compiler更加友好,以AST(Abstract Syntax Tree)的方式对template中的HTML重新组织之后,我们可以获得以下的JSON数据

{ name:'form', children:[ { name:'div', children:[{text:'Hello'},{text:''}] }, { name:'input', attrs:[['ngModel','']] } ] } 

以上的数据表示了HTML,相当简单易懂。而template中的binding可以用以下的JSON表示

{ text:'', expr:{propPath:['user','name']}, line:2, col:14 } 

text代表着初始化的数据,因为依赖ts代码中的输入,所以默认为空。expr包含着Angular程序中在template的表达式信息,propPath中包含着数据的路径,当在expression中使用pipe或者*ngFor等代码时 ,expr中包含的内容会有更复杂的表现,linecol保留了binding中原始的位置信息,这点很重要,当template报错的时候可以精确告诉开发者template中哪一行代码发生了问题,如果你开发过Angular 2程序,你一定见过这种报错:

Uncaught EXCEPTION: Error in hello_comp.html: 2:14 Uncaught TypeError: Cannot read property 'name' of undefined 

现在我们的Parser已经可以解析出Component的内容了,对于Directive又该如何表示呢,我们仍然可以在AST JSON中进行表示

{ name:'input', attrs:[['ngModel','']], directives:[ { ctor:NgModel, deps:[NgForm] } ] } 

以上我们已经将所有的代码parse成了对compiler友好的AST JSON格式,下一步就是将parse得到的数据进行实例化,让App可以真正运行起来。

Instantiate(实例化)

首先介绍NgElement的数据结构,NgElement是Angular 2中很重要的一部分,负责将AST转化回DOM结构,并完成相应的binding和Directive等内容的实例化。

class NgElement{ parrent:ngElement; doEl:HTMLElment; directives:map; constructor(parent:NgElment,ast:ElementAst){ this.domEl = document.createElement(ast.name); ast.attrs.forEach((atrr)=>{ this.domEl.setAttribute(attr[0],atrr[1]); }) parent.domEl.appendChild(this.domeEl); } } 

NgElement中的这部分代码相当简单,生成了AST JSON中对应的DOM结构,并添加了对应attribute,针对Directive部分的处理如下,逐层实例化AST JSON中的ctor和deps

class NgElement{ parrent:ngElement; doEl:HTMLElment; directives:map; getDirectiveDep(dirType){ if(this.directives.has(dirType)){ return this.directives.get(dirType); } return this.parent.getDirectiveDep(dirType); } createDirective(dirAst){ var deps = ast.deps.map((depType)=>this.getDirectiveDep(depType)); this.directives.set(ast.ctor,new ast.ctor(...deps)); } } 

针对于Template中的binding部分,通过AST JSON中的expr表达式来进行脏值检查,并将数据存储在target中,在Angular 2中最简单取出该值的方式就是<div #testDiv></div>testDiv就是binding中的target,所有的数据都会存储在target中。

class Binding{ target: Node; targetProp: string; expr: BindingAST; lastValue: any; check(component:any){ try{ var newValue = evaluate(this.expr,component); if(newValue !== this.lastValue){ this.target[this.targetProp] = this.lastValue = newValue; } } catch(e){ throw new Error(`Error in ${this.expr.line}:${this.expr.col}:${e.message}`); } } } 

最后我们会有View类来整合NgElement和binding中的脏值检查

class View{ component:any; ngElements: NgElment[]; bindings: Binding[]; dirtyCheck(){ this.bindigns.forEach(binding=>binding.check(this.component)); } } 

通过以上的步骤,我们可以将Parser生成的AST转化为可以运行的App,然而Compiler的功能不仅仅是将源代码转换AST再转换为可运行程序,在compile的过程中对性能进行优化也是很重要的一步。

Compiler性能优化

在NgElement对Directive处理的代码中,我们看到其中directives的类型是Map,如果我们将所有的directives都列举出来,将代码转换为

class InlineNgElement{ ... dir0,dir1,...:any; dirType0,dirType1,...:any; getDirecitveDep(dirType){ if(type === this.dirType0) return this.dir0; if(type === this.dirType1) return this.dir1; ... } } 

这样的代码看起来可能很奇怪,多层If语句的可能会影响函数性能,但是Javascript V8的虚拟机的Fast Property Access via Hidden Classes机制却可以将这类代码进行很好的优化从而获得更高的性能。

我们将NgElement转换为InlineNgElement以获得更高的性能,然而我们的View类中却仍然含有大量的Array,如何让View也利用V8虚拟机的Fast Property优化,其实方法也很明确:我们只需要按正确的顺序初始化DOM,并且在Directive的初始化过程中也依照正确的顺序,保证被依赖的directive先被初始化生成就可以了。

初始化DOM结构

function HelloCompView(component){ this.component = component; this.node0 = document.createElement('form'); ths.node1 = document.createElement('div'); this.node0.appendChild(this.node1); } 

初始化Directive

function HelloCompView(component){ ... this.dir0 = new NgForm(); ... this.dir1 = new NgModel(this.dir0); } 

再讲binding中的dirtyCheck对应到相应的node

HelloCompView.prototype.dirtyCheck = function(){ var v = this.component.user.name; if(v !== this.exprVal0){ this.node3.ngModel = v; this.exprVal0 = v; } } 

通过以上的步骤,我们View全部可以利用Fast Property特性进行优化,当然所有的component的代码都需要根据component中directive和expression等内容单独生成,我们需要针对每个component生成单独的compile代码

class CompileElement{ domElProp:string = new PropertyVar(); stmts: string[]; constructor(parent:CompileElement,ast:ElementAst){ this.stmts = [` this.${domElProp} = document.createElement('${ast.name}'); this.${parent.domElProp}.appendChild(${this.domElProp}); `] } } 

在以上的代码中,我们展示了Angular的Parser和Instantiate是如何协同工作的,通过优化Instantiate的代码,利用V8虚拟机的性能优化,Angular 2再次获得了将近1倍的性能提升。

然而有一个问题被我们忽略了,我们应该使用什么作为Parser? 使用浏览器是个很好的主意,浏览器很适合用于解析HTML,在Angular 1和Angular 2中我们也的确可以使用浏览器作为Parser,这也就是JIT(Just In Time)编译的部分,所有的Compile过程全都是在浏览器端进行的。

《Angular 2 中的编译器与预编译(AoT)优化》

如果我们可以将CompileElement的过程放在Server端,那浏览器端承载的工作量就会大幅度减少,相应的页面加载时间也会大幅度减少

《Angular 2 中的编译器与预编译(AoT)优化》

Angular团队已经实现了可以在server端对代码进行parse的工具:compiler-cli

官方提供的angular-cli通过ng serve --aotng build --prod --aot也支持实时aot的实时预览与生产代码生成,github上的angular2-aot-webpack项目提供了简单的webpack实现

用作者手中的一个Angular项目比对一下JIT和AOT的性能

JIT Compile

《Angular 2 中的编译器与预编译(AoT)优化》

AOT Compile

《Angular 2 中的编译器与预编译(AoT)优化》

效果感人

支持AOT

AOT优化虽然带来了相当大的性能提升,但是由于AOT的特性,部分在JIT模式下可用的方法在AOT下是不可行或者官方不建议的,在github上的webpack2-starter总结了会导致AOT编译失败的情况:

  • Don’t use require statements for your templates or styles, use styleUrls and templateUrls, the angular2-template-loader plugin will change it to require at build time.
  • Don’t use default exports.
  • Don’t use form.controls.controlName, use form.get(‘controlName’)
  • Don’t use control.errors?.someError, use control.hasError(‘someError’)
  • Don’t use functions in your providers, routes or declarations, export a function and then reference that function name
  • Inputs, Outputs, View or Content Child(ren), Hostbindings, and any field you use from the template or annotate for Angular should be public

It’s just “Angular”

尾巴

Angular官方在2016年12月13日宣布了一个非常”耸人听闻”的消息:将在2017年3月份跳过3.0版本正式release Angular 4.0。不过Angular官方随后快速放出了定心丸,4.0版本只不过是Angular团队将命名方式切换为Semantic Versioning(SEMVER),并且向下兼容2.0,这么一看就很容易理解了,React的版本号都已经15了,Angular的版本到4.0也没有什么大惊小怪的。

另外一个问题就是3.0版本去哪了,一路从rc版本使用Angular 2.0的用户都知道@angular/router曾经废弃掉了一个版本,这样目前的版本号就变得很尴尬,@angular/core,@angular/compiler,@angular/http等版本号都是保持一致的,而@angular/router的版本号却永远高出一个版本,当主版本号是2.3.1时,router的版本号却已经是3.3.1了,为了保持版本一致,Angular将越过3.0版本直接统一从4.0开始。

为了避免各种Angular版本号给开发者造成不必要的误解,也为了避免整个社区割裂,Angular团队号召大家在非必要情况下忽略版本号,比如:我是一个Angular开发者,这是一个Angular会议,Angular的生态系统发展很快等等。在培训和介绍的时候使用版本号,例如本文介绍的内容就是针对于Angular 2版本的。

参考资料

[1​]: juristr.com/blog/2016/1… “it’s going to be Angular 4.0, or simply Angular”
[2​]: youtu.be/kW9cJsvcsGo “The Angular 2 Compiler Tobias Bosch”
[​3]: blog.mgechev.com/2016/08/14/… “Ahead-of-Time Compilation in Angular”
[​4]: angular.io/docs/ts/lat… “AHEAD-OF-TIME COMPILATION”
[5​]: github.com/v8/v8/wiki/… “V8 Design Elements”

    原文作者:小蜜蜂
    原文地址: https://juejin.im/entry/58636f198d6d81006500ae27
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞