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