Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AOT编译 #8

Open
deepthan opened this issue Dec 21, 2017 · 0 comments
Open

AOT编译 #8

deepthan opened this issue Dec 21, 2017 · 0 comments

Comments

@deepthan
Copy link
Owner

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant