deepthan/blog-angular

AOT编译

deepthan opened this issue · 0 comments

1. ahead-of time 预编译 AOT

开发者可以在构造时(build-time)编译angular应用。通过compiler-cli、ngc编译应用程序,应用可以从一个模块工厂直接启动,意味着不再需要把angular编译器添加到JavaScript包中,预编译的应用程序加载加速,具有更高的性能。(compiler :编译器)

为什么要进行预编译?

编译可以让Angular应用达到更高层度的运行效率,主要是指的性能提升,但也包括电池节能和节省流量。
Angular采用了一个不同的方式。在给每个组件做渲染和变化检测的时候,它不再使用同一套逻辑,框架在运行时或者编译时会生成对js虚拟机友好的代码。这些友好的代码可以让js虚拟机在属性访问的缓存,执行变化检查,进行渲染的逻辑执行的快的多。

什么东西会被编译?

把组件的模板编译成一个JS类,这些类包含了在绑定的数据中检测变化和渲染UI的逻辑。

JiT编译模式的流程

非AoT应用的开发流程大概是:

  • 使用TypeScript开发Angular应用
  • 使用tsc来编译这个应用的ts代码
  • 打包
  • 压缩
  • 部署

部署好,在页面打开这个app:

  • 浏览器下载js代码
  • Angular启动
  • Angular在浏览器中开始JiT编译的过程,例如生成app中各个组件的js代码
  • 应用页面得以渲染

AoT编译模式的流程

使用AoT模式的应用的开发流程是:

  • 使用TypeScript开发Angular应用
  • 使用ngc来编译应用
    • 使用Angular编译器对模板进行编译,生成TypeScript代码
    • TypesScript代码编译为JavaScript代码
  • 打包
  • 压缩
  • 部署
    虽然前面的过程稍稍复杂,但是用户这一侧的事情就变简单了:
  • 下载所有代码
  • Angular启动
  • 页面渲染

Jit和AoT的主要区别

  • 编译过程发生的时机
  • JiT生成的是JS代码,而AoT生成的是TS代码。这主要是因为JiT是在浏览器中进行的,它完全没必要生成TS代码,而是直接生产了JS代码。

深入AoT编译

如果你对编译器的词法分析过程,解析和生成代码过程等感兴趣,你可以读一读Tobias Bosch的《Angular2编译器》一文,或者它的胶片。

《Angular2编译器》一文链接 https://www.youtube.com/watch?v=kW9cJsvcsGo

它的胶片链接 https://speakerdeck.com/mgechev/angular-toolset-support?slide=69

Angular模板编译器收到一个组件和它的上下文作为输入,并产生了如下文件:

  • *.ngfactory.ts

  • *.css.shim.ts : 样式作用范围被隔离后的css文件,根据组件所设置的ViewEncapsulation模式不同而会有不同

  • *.metadata.json :当前组件/模块的装饰器元数据信息,这些数据可以被想象成以json格式传递给 @component @NgModule 装饰器的信息。

    '*'是一个文件名占位符,例如对于hero.component.ts这样的组件,编译器生成的文件是 hero.component.ngfactory.ts, hero.component.css.shim.ts 和 hero.component.metadata.json。*.css.shim.ts和我们讨论的主题关系不大,因此不会对它详细描述。

*.ngfactory.ts 的内部结构

它包含了如下的定义:

  • _View_{COMPONENT}_Host{COUNTER} 我们称之为internal host component
  • _View_{COMPONENT}{COUNTER} 我们称之为 internal component

以及下面两个函数

  • viewFactory_{COMPONENT}_Host{COUNTER}
  • viewFactory_{COMPONENT}{COUNTER}
    其中的 {COMPONENT} 是组件的控制器名字,而 {COUNTER} 是一个无符号整数。他们都继承了 AppView,并且实现了下面的方法:
  • createInternal 组件的渲染器
  • destroyInternal 执行事件监听器等的清理
  • detectChangesInternal 以内联缓存优化后的逻辑执行变化检测

上述这些工厂函数只在生成的AppView实例中才存在。

detectChangesInternal中的代码是JS虚拟机友好的。

<div>{{newName}}</div>
<input type="text" [(ngModel)]="newName">

我们来看看编译后这个模板的代码,detectChangesInternal方法的代码看起来像是这样的:

// ...
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);
// ...

假设currVal_6的值是3,this_expr_6的值是1,我们来跟踪看看这个方法的执行。对于这样的一个调用 import4.checkBinding(1, 3),在生产环境下,checkBinding 执行的是下面的检查:

1 === 3 || typeof 1 === 'number' && typeof 3 === 'number' && isNaN(1) && isNaN(3);

上述表达式返回false,因此我们将把变化保持下来,以及直接更新 NgModel 的属性 model 的值,在这之后,detectContentChildrenChanges 方法会被调用,它将为整个模板内容的子级调用 detectChangesInternal。一旦 NgModel 指令发现了 model 属性发生了变化,它就会(几乎)直接调用渲染器来更新对应的DOM元素。

目前为止,我们还没有碰到任何特殊的,或者特别复杂的逻辑。

context 属性

也许你已经注意到了在internal component内部访问了 this.context 属性。
internal component中的 context 是这个组件的控制器的实例,例如这样的一个组件:

@Component({
  selector: 'hero-app',
  template: '<h1>{{ hero.name }}</h1>'
})
class HeroComponent {
  hero: Hero;
}

this.context 就是 new HeroComponent(),这意味着如果在 detectChangesInternal 中我们需要访问 this.context.name 的话,就带来了一个问题: 如果我们使用AoT模式编译组件的模板,由于这个模式会生成TypeScript代码,因此我们要确保在组件的模板中只访问 this.context 中的public成员。 这是为何?由于TypeScript的类属性有访问控制,强制类外部只能访问类(及其父类)中的public成员,因此在internal component内部我们无法访问 this.context 的任何私有成员。因此,下面这个组件:

@Component({
  selector: 'hero-app',
  template: '<h1>{{ hero.name }}</h1>'
})
class HeroComponent {
  private hero: Hero;
}

以及这个组件

class Hero {
  private name: string;
}

@Component({
  selector: 'hero-app',
  template: '<h1>{{ hero.name }}</h1>'
})
class HeroComponent {
  hero: Hero;
}

在生成出来的 *.ngfactory.ts 中,都会抛出编译错误。第一个组件代码,internal component无法访问到在 HeroComponent 类中被声明为 private 的 hero 属性。第二个组件代码中,internal component无法访问到 hero.name 属性,因为它在 Hero 类中被声明为private。

AoT与封装

在Angular的源码中,我们可以找到解决的办法,使用TypeScript的 /** @internal */ 注释声明,就能够达到既保证组件代码对AoT友好,又能够确保组件的封装良好的目的。

// component.ts
@Component({
  selector: 'third-party',
  template: `
    {{ initials }}
  `
})
class ThirdPartyComponent {
  /** @internal */
  initials: string;
  private _name: string;

  @Input()
  set name(name: string) {...}
}

initials 属性仍然是public的。我们在使用 tsc 编译这个组件时,设置 --stripInternal 和 --declarations 参数,initials 属性就会从组件的类型定义文件(即 .d.ts 文件)中被删掉。这样我们就可以做到在我们的类库内部使用它,但是我们的组件使用者无法使用它。

参考地址

https://mp.weixin.qq.com/s?__biz=MzIwMTYyMDEyMg%3D%3D&mid=2247483745&idx=1&sn=3fcb189b1d6b06b1a3311f3d9d532262&scene=45#wechat_redirect