阅读 Angular 2 的初始化和组件编译方面的源代码
在年底的活动中,我有几次介绍了Angular 2的内容,但是我发现自己一边说着一边并没有很好地理解。我一直想在有时间的时候好好地理解它!为了理解它,只能阅读源代码了!所以我打算读一下Angular 2的源代码。
由於使用方法在官方文件以及其他地方都有詳細介紹,所以我們將專注於內部結構的研究。由 @laco0416 負責處理 Change Detection 和 Dependency Injection 相關的部分/將在12/23完成,所以我想嘗試解決初始化和Component編譯的問題。
在 AngularConnect 的主题演讲中,有人提到在 Angular 1 中,HTML 的编译在运行时经常被执行,但是在 Angular 2 中,它只会在初始化时执行一次,这促进了速度的提升。我对这方面的实际情况很感兴趣。
版本是beta.0刚发布后的2a2f9a9a196cf1502c3002f14bd42b13a4dcaa53。基本上可以认为是与作为beta.0发布的版本相同。
简而言之
-
- Angular 2 はプラットフォームごとに様々なコンポーネントを切り替えられるようになっている。
-
- Component のコンパイルの結果、複数の Command ができる。画面を描画するための命令群のようなもの。
-
- RuntimeCompiler がトップレベル Component 型を DOM 関係なくコンパイルして ProtoViewRef を返す。View を作るための雛形のようなもの。
- AppViewManagaer が ProtoViewRef(コンパイルされた Component のメタ情報)をもとに Component のインスタンスを作り、DOM要素に紐付ける。
在阅读资料之前
首先,Angular 2的代码库位于GitHub的angular/angular存储库中。其中的modules目录包含了一些项目,如angular2_material等。主要的源代码位于modules/angular2中。
在非本体源代码中,可以使用 modules/playground 的示例和 modules/angular2/src/test。两者都十分丰富,因此可以理解源代码是困难的,可以查看详细的用法和规范。playground 中的 HTML 中包含特殊字符,如 $SCRIPTS$,在构建时会被填充。如果不理解,请检出 angular/angular 并尝试构建。请注意,angular/angular 只兼容 Node 4 和 npm 2 系列。
我使用 Visual Studio Code 阅读源代码。利用 TypeScript 的类型信息可以跳转到定义处,还可以搜索引用,非常方便。
尝试阅读 Bootstrap。
我们从初始化应用程序开始,以下路径都是在modules目录下讨论的。让我们来看一下具有浏览器启动的angular2/platform/browser.ts。
export function bootstrap(
appComponentType: Type,
customProviders?: Array<any /*Type | Provider | any[]*/>): Promise<ComponentRef> {
reflector.reflectionCapabilities = new ReflectionCapabilities();
let appProviders =
isPresent(customProviders) ? [BROWSER_APP_PROVIDERS, customProviders] : BROWSER_APP_PROVIDERS;
return platform(BROWSER_PROVIDERS).application(appProviders).bootstrap(appComponentType);
}
在 bootstrap 中,我们传递一个顶层组件的类型以及(如果需要的话)自定义的提供程序。以下是处理流程的示例。
-
- 平台:通过BROWSER_PROVIDERS创建PlatformRef。
PlatformRef#application:使用BROWSER_APP_PROVIDERS加上自定义的providers创建ApplicationRef。
ApplicationRef#bootstrap:传递顶层Component的类型来启动应用程序。
我们来看一下这些方法。
顺便提一下,BROWSER_PROVIDERS 是一个加入了对 DOM 适配器初始化处理的平台共有提供程序的模式。
/**
* A set of providers to initialize the Angular platform in a web browser.
*
* Used automatically by `bootstrap`, or can be passed to {@link platform}.
*/
export const BROWSER_PROVIDERS: Array<any /*Type | Provider | any[]*/> = CONST_EXPR([
PLATFORM_COMMON_PROVIDERS,
new Provider(PLATFORM_INITIALIZER, {useValue: initDomAdapter, multi: true}),
]);
平台
当追溯import/export时,我们发现平台(platform)位于angular2/src/core/application_ref.ts文件中。顺便提一下,平台的参数是BROWSER_PROVIDERS。简而言之,只是调用了_createPlatform函数。
function _createPlatform(providers?: Array<Type | Provider | any[]>): PlatformRef {
_platformProviders = providers;
let injector = Injector.resolveAndCreate(providers);
_platform = new PlatformRef_(injector, () => {
_platform = null;
_platformProviders = null;
});
_runPlatformInitializers(injector);
return _platform;
}
创建一个新的 Injector,使用它创建了一个名为 PlatformRef_ 的东西,在执行每个平台的初始化处理后,返回 PlatformRef_。
平台参考_
让我们来看看 PlatformRef_。
constructor(private _injector: Injector, private _dispose: () => void) { super(); }
您只是将参数作为成员变量保存下来。第二个参数似乎是指应用程序关闭时的处理。
平台引用_#引导程序.
让我们来看一下在bootstrap中被调用的application方法。这里提到的providers是指BROWSER_APP_PROVIDERS和自定义providers的组合。
application(providers: Array<Type | Provider | any[]>): ApplicationRef {
var app = this._initApp(createNgZone(), providers);
return app;
}
创建了一个NgZone,并将其传递给_initApp。
private _initApp(zone: NgZone, providers: Array<Type | Provider | any[]>): ApplicationRef {
var injector: Injector;
var app: ApplicationRef;
zone.run(() => {
providers = ListWrapper.concat(providers, [
provide(NgZone, {useValue: zone}),
provide(ApplicationRef, {useFactory: (): ApplicationRef => app, deps: []})
]);
var exceptionHandler;
try {
injector = this.injector.resolveAndCreateChild(providers);
exceptionHandler = injector.get(ExceptionHandler);
zone.overrideOnErrorHandler((e, s) => exceptionHandler.call(e, s));
} catch (e) {
if (isPresent(exceptionHandler)) {
exceptionHandler.call(e, e.stack);
} else {
print(e.toString());
}
}
});
app = new ApplicationRef_(this, zone, injector);
this._applications.push(app);
_runAppInitializers(injector);
return app;
}
在构建和返回 ApplicationRef 时,可以看到它继承了包含 BROWSER_PROVIDERS 的注入器,并添加了 BROWSER_APP_PROVIDERS + 自定义 providers ,从而创建了一个子注入器。
NgZone#run是什么?
浏览器应用程序供应商
顺便提一下,BROWSER_APP_PROVIDERS 是在浏览器上运行的应用程序所需提供者的模样。BROWSER_APP_COMMON_PROVIDERS 和 COMPILER_PROVIDERS 的分离是为了离线编译铺设基础?
/**
* An array of providers that should be passed into `application()` when bootstrapping a component.
*/
export const BROWSER_APP_PROVIDERS: Array<any /*Type | Provider | any[]*/> = CONST_EXPR([
BROWSER_APP_COMMON_PROVIDERS,
COMPILER_PROVIDERS,
new Provider(XHR, {useClass: XHRImpl}),
]);
BROWSER_APP_COMMON_PROVIDERS 的内容如下所示。
/**
* A set of providers to initialize an Angular application in a web browser.
*
* Used automatically by `bootstrap`, or can be passed to {@link PlatformRef.application}.
*/
export const BROWSER_APP_COMMON_PROVIDERS: Array<any /*Type | Provider | any[]*/> = CONST_EXPR([
APPLICATION_COMMON_PROVIDERS,
FORM_PROVIDERS,
new Provider(PLATFORM_PIPES, {useValue: COMMON_PIPES, multi: true}),
new Provider(PLATFORM_DIRECTIVES, {useValue: COMMON_DIRECTIVES, multi: true}),
new Provider(ExceptionHandler, {useFactory: _exceptionHandler, deps: []}),
new Provider(DOCUMENT, {useFactory: _document, deps: []}),
new Provider(EVENT_MANAGER_PLUGINS, {useClass: DomEventsPlugin, multi: true}),
new Provider(EVENT_MANAGER_PLUGINS, {useClass: KeyEventsPlugin, multi: true}),
new Provider(EVENT_MANAGER_PLUGINS, {useClass: HammerGesturesPlugin, multi: true}),
new Provider(DomRenderer, {useClass: DomRenderer_}),
new Provider(Renderer, {useExisting: DomRenderer}),
new Provider(SharedStylesHost, {useExisting: DomSharedStylesHost}),
DomSharedStylesHost,
Testability,
BrowserDetails,
AnimationBuilder,
EventManager
]);
应用参考
我们现在来看一下 ApplicationRef_。
constructor(private _platform: PlatformRef_, private _zone: NgZone, private _injector: Injector) {
super();
if (isPresent(this._zone)) {
ObservableWrapper.subscribe(this._zone.onTurnDone,
(_) => { this._zone.run(() => { this.tick(); }); });
}
this._enforceNoNewChanges = assertionsEnabled();
}
在中文中对以下内容进行释义,只需提供一种选项:
NgZone#onTurnDone 是什么意思?
/**
* Notifies subscribers immediately after Angular zone is done processing
* the current turn and any microtasks scheduled from that turn.
*
* Used by Angular as a signal to kick off change-detection.
*/
get onTurnDone() { return this._onTurnDoneEvents; }
听说,在发生 AJAX 或 DOM 事件后会被调用吧。在 tick() 函数中似乎会触发变更检测。
Zone.js 的详细信息:
应用参照_#引导程序
然后是最后一步的引导程序。
bootstrap(componentType: Type,
providers?: Array<Type | Provider | any[]>): Promise<ComponentRef> {
var completer = PromiseWrapper.completer();
this._zone.run(() => {
var componentProviders = _componentProviders(componentType);
if (isPresent(providers)) {
componentProviders.push(providers);
}
var exceptionHandler = this._injector.get(ExceptionHandler);
this._rootComponentTypes.push(componentType);
try {
var injector: Injector = this._injector.resolveAndCreateChild(componentProviders);
var compRefToken: Promise<ComponentRef> = injector.get(APP_COMPONENT_REF_PROMISE);
var tick = (componentRef) => {
this._loadComponent(componentRef);
completer.resolve(componentRef);
};
var tickResult = PromiseWrapper.then(compRefToken, tick);
// THIS MUST ONLY RUN IN DART.
// This is required to report an error when no components with a matching selector found.
// Otherwise the promise will never be completed.
// Doing this in JS causes an extra error message to appear.
if (IS_DART) {
PromiseWrapper.then(tickResult, (_) => {});
}
PromiseWrapper.then(tickResult, null,
(err, stackTrace) => completer.reject(err, stackTrace));
} catch (e) {
exceptionHandler.call(e, e.stack);
completer.reject(e, e.stack);
}
});
return completer.promise.then(_ => {
let c = this._injector.get(Console);
let modeDescription =
assertionsEnabled() ?
"in the development mode. Call enableProdMode() to enable the production mode." :
"in the production mode. Call enableDevMode() to enable the development mode.";
c.log(`Angular 2 is running ${modeDescription}`);
return _;
});
}
看起来,虽然比较复杂,但最终需要创建更多的子注入器,获取ComponentRef,并使用它来_loadComponent,并以此来解析结果的Promise。
_loadComponent(ref): void {
var appChangeDetector = internalView(ref.hostView).changeDetector;
this._changeDetectorRefs.push(appChangeDetector.ref);
this.tick();
this._rootComponents.push(ref);
this._bootstrapListeners.forEach((listener) => listener(ref));
}
如果ref.hostView及其他要素存在,那么ComponentRef就是已编译过的组件模式。它是如何编译的?是从注入器中获取ComponentRef对象吗?
当我搜索 APP_COMPONENT_REF_PROMISE,我找到了相关信息。
function _componentProviders(appComponentType: Type): Array<Type | Provider | any[]> {
return [
provide(APP_COMPONENT, {useValue: appComponentType}),
provide(APP_COMPONENT_REF_PROMISE,
{
useFactory: (dynamicComponentLoader: DynamicComponentLoader, appRef: ApplicationRef_,
injector: Injector) => {
// Save the ComponentRef for disposal later.
var ref: ComponentRef;
// TODO(rado): investigate whether to support providers on root component.
return dynamicComponentLoader.loadAsRoot(appComponentType, null, injector,
() => { appRef._unloadComponent(ref); })
.then((componentRef) => {
ref = componentRef;
if (isPresent(componentRef.location.nativeElement)) {
injector.get(TestabilityRegistry)
.registerApplication(componentRef.location.nativeElement,
injector.get(Testability));
}
return componentRef;
});
},
deps: [DynamicComponentLoader, ApplicationRef, Injector]
}),
provide(appComponentType,
{
useFactory: (p: Promise<any>) => p.then(ref => ref.instance),
deps: [APP_COMPONENT_REF_PROMISE]
}),
];
}
忽略TestabilityRegistry,根据DynamicComponentLoader#loadAsRoot的功能来看,它接受顶层组件的类型并创建ComponentRef。
动态组件加载器#作为根加载
loadAsRoot(type: Type, overrideSelector: string, injector: Injector,
onDispose?: () => void): Promise<ComponentRef> {
return this._compiler.compileInHost(type).then(hostProtoViewRef => {
var hostViewRef =
this._viewManager.createRootHostView(hostProtoViewRef, overrideSelector, injector);
var newLocation = this._viewManager.getHostElement(hostViewRef);
var component = this._viewManager.getComponent(newLocation);
var dispose = () => {
if (isPresent(onDispose)) {
onDispose();
}
this._viewManager.destroyRootHostView(hostViewRef);
};
return new ComponentRef_(newLocation, component, type, injector, dispose);
});
}
-
- 编译器#在主机上编译Component类型并创建ProtoViewRef。
-
- AppViewManager#创建根主机视图:从ProtoViewRef中创建Component实例,并将其与在全局视图(如浏览器的文档)中与Component选择器(可覆盖)匹配的第一个元素相关联。返回的是HostViewRef。
-
- AppViewManager#获取主机元素:(省略)
- AppViewManager#获取Component:(省略)
TODO: HostViewRef 和 ElementRef 的区别?后者似乎指向 DOM 元素。前者呢?
编译器_#在主机上编译
终于快到核心了。
compileInHost(componentType: Type): Promise<ProtoViewRef> {
var metadatas = reflector.annotations(componentType);
var compiledHostTemplate = metadatas.find(_isCompiledHostTemplate);
if (isBlank(compiledHostTemplate)) {
throw new BaseException(
`No precompiled template for component ${stringify(componentType)} found`);
}
return PromiseWrapper.resolve(this._createProtoView(compiledHostTemplate));
}
哎?已经假设Metadata已经编译了?有什么不对劲…
当在angular2/src/compiler/compiler.ts中进行搜索时,COMPILER_PROVIDERS的设置如下所示。
export const COMPILER_PROVIDERS: Array<Type | Provider | any[]> = CONST_EXPR([
// ...
new Provider(RuntimeCompiler, {useClass: RuntimeCompiler_}),
new Provider(Compiler, {useExisting: RuntimeCompiler}),
// ...
]);
在 Compiler 型的 DI 中,使用的不是 Compiler_ 而是 RuntimeCompiler_。RuntimeCompiler_ 继承自 Compiler_。
因为“Runtime”和“Compiler”都有在名称中的意思,所以应该还有其他不是“Runtime”的编译器存在,所以也许也会提供离线使用的编译器(在AngularConnect的主题演讲中也提到了离线编译将会在不久的将来实现)。Compiler_是用于使用离线编译的模板的吗?
运行时编译器
我会重新调整心态,继续追踪 RuntimeCompiler。
compileInHost(componentType: Type): Promise<ProtoViewRef> {
return this._templateCompiler.compileHostComponentRuntime(componentType)
.then(compiledHostTemplate => internalCreateProtoView(this, compiledHostTemplate));
}
-
- TemplateCompiler#compileHostComponentRuntime: 这正是 Angular 2 编译的过程!
internalCreateProtoView: 经过追踪,发现调用了 ProtoViewFactory#createHost。基本上只是将编译后的 CompiledHostTemplate 注册到 Renderer 中,然后再封装在 AppProtoView 中。
模板编译器#编译主机组件运行时
我看一下 TemplateCompiler。
compileHostComponentRuntime(type: Type): Promise<CompiledHostTemplate> {
var hostCacheKey = this._hostCacheKeys.get(type);
if (isBlank(hostCacheKey)) {
hostCacheKey = new Object();
this._hostCacheKeys.set(type, hostCacheKey);
var compMeta: CompileDirectiveMetadata = this._runtimeMetadataResolver.getMetadata(type);
assertComponent(compMeta);
var hostMeta: CompileDirectiveMetadata =
createHostComponentMeta(compMeta.type, compMeta.selector);
this._compileComponentRuntime(hostCacheKey, hostMeta, [compMeta], new Set());
}
return this._compiledTemplateDone.get(hostCacheKey)
.then(compiledTemplate => new CompiledHostTemplate(compiledTemplate));
}
-
- 获取Component的元数据。
创建主机Component的元数据。
这里是主要的编译处理!
TemplateCompiler#_compileComponentRuntime的汇编运行时
我看到一个相当大的人物出现了。
private _compileComponentRuntime(
cacheKey: any, compMeta: CompileDirectiveMetadata, viewDirectives: CompileDirectiveMetadata[],
compilingComponentCacheKeys: Set<any>): CompiledComponentTemplate {
let uniqViewDirectives = removeDuplicates(viewDirectives);
var compiledTemplate = this._compiledTemplateCache.get(cacheKey);
var done = this._compiledTemplateDone.get(cacheKey);
if (isBlank(compiledTemplate)) {
var styles = [];
var changeDetectorFactory;
var commands = [];
var templateId = `${stringify(compMeta.type.runtime)}Template${this._nextTemplateId++}`;
compiledTemplate = new CompiledComponentTemplate(
templateId, (dispatcher) => changeDetectorFactory(dispatcher), commands, styles);
this._compiledTemplateCache.set(cacheKey, compiledTemplate);
compilingComponentCacheKeys.add(cacheKey);
done = PromiseWrapper
.all([<any>this._styleCompiler.compileComponentRuntime(compMeta.template)].concat(
uniqViewDirectives.map(dirMeta => this.normalizeDirectiveMetadata(dirMeta))))
.then((stylesAndNormalizedViewDirMetas: any[]) => {
var childPromises = [];
var normalizedViewDirMetas = stylesAndNormalizedViewDirMetas.slice(1);
var parsedTemplate = this._templateParser.parse(
compMeta.template.template, normalizedViewDirMetas, compMeta.type.name);
var changeDetectorFactories = this._cdCompiler.compileComponentRuntime(
compMeta.type, compMeta.changeDetection, parsedTemplate);
changeDetectorFactory = changeDetectorFactories[0];
var tmpStyles: string[] = stylesAndNormalizedViewDirMetas[0];
tmpStyles.forEach(style => styles.push(style));
var tmpCommands: TemplateCmd[] = this._compileCommandsRuntime(
compMeta, parsedTemplate, changeDetectorFactories,
compilingComponentCacheKeys, childPromises);
tmpCommands.forEach(cmd => commands.push(cmd));
return PromiseWrapper.all(childPromises);
})
.then((_) => {
SetWrapper.delete(compilingComponentCacheKeys, cacheKey);
return compiledTemplate;
});
this._compiledTemplateDone.set(cacheKey, done);
}
return compiledTemplate;
}
我正在创建并返回一个名为CompiledCompoentTemplate的东西。
@CONST()
export class CompiledComponentTemplate {
constructor(public id: string, public changeDetectorFactory: Function,
public commands: TemplateCmd[], public styles: string[]) {}
}
改变检测器工厂的实体、命令和样式在返回编译组件模板后会被填充,这是一个有趣的地方。
以下是实际的编译处理过程。
-
- StyleCompiler#compileComponentRuntime: 将CSS解析为抽象语法树(AST)。
TemplateCompiler#normalizeDirectiveMetadata: 将模板中使用的指令(如ngFor等PLATFORM_DIRECTIVES中定义的指令和通过directives指定的指令)的元数据进行规范化。
TemplateParser#parse: 利用2将模板解析为AST。结果为TemplateAST[]。同时创建ChangeDetector的工厂函数。
TemplateCompiler#_compileCommandsRuntime: 根据前面创建的AST等信息,生成命令(TemplateCmd[])。在其中递归地编译子组件。
看起来您想要创建的是 TemplateCmd[] 这样的东西。
“Command究竟是什么意思?”
export interface TemplateCmd extends RenderTemplateCmd {
visit(visitor: RenderCommandVisitor, context: any): any;
}
TemplateCmd 的父母是…
/**
* Abstract base class for commands to the Angular renderer, using the visitor pattern.
*/
export abstract class RenderTemplateCmd {
abstract visit(visitor: RenderCommandVisitor, context: any): any;
}
繼承這個抽象類別的包括以下內容。
-
- RenderBeginCmd: Command to begin rendering.
-
- RenderTextCmd: Command to render text.
-
- RenderNgContentCmd: Command to render projected content.
-
- RenderBeginElementCmd: Command to begin rendering an element.
-
- RenderBeginComponentCmd: Command to begin rendering a component.
- RenderEmbeddedTemplateCmd: Command to render a component’s template.
这很令人惊讶。看起来“Command”似乎是指用于渲染的指令。总结来说,Angular 2 在将组件的模板解析为AST后,将其转换为用于绘制HTML(在浏览器中)的指令集。这被称为编译过程。
终结
这次在这里时间到了,但是(如果有的话)下次我们想要编译并查看如何使用生成的命令。让我们一起来解析Angular 2的内部结构吧!