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

    xiaoxiao2026-04-16  2

    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中包含的内容会有更复杂的表现,line和col保留了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过程全都是在浏览器端进行的。

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

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

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

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

    JIT Compile

    AOT Compile

    效果感人

    支持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 nameInputs, 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版本的。

    参考资料
    相关资源:Angular介绍、安装Angular Cli、创建Angular项目 预编译器Scss Less css配置
    最新回复(0)