Angular 2 の初期化・Component のコンパイル周りのソースを読んでみる


年末のイベントで何度か Angular 2 について紹介させていただいたのですが、自分で話しながらよくわかっていないなと感じることしきり。時間ができたらしっかり理解したい！と思っていました。理解するにはソースを読むしかない！ということで Angular 2 のソースを読んでみます。

使い方については本家ドキュメントを始めいろいろなところに書かれているので、内部構造について調べます。Change Detection, Dependency Injection 周りは @laco0416 さんによって[カバーされている](http://qiita.com/laco0416/items/78edd53f5da8ead02e75)/される予定（12/23）なので、初期化・Component のコンパイル周りに挑戦してみたいと思います。

AngularConnect のキーノートでは、Angular 1 ではランタイムで何度も HTML のコンパイルが走っていたが、Angular 2 では初期化時に一度走るだけになり、高速化につながっているという話がありました。このあたり実際にどうなっているのか興味がありますね。

バージョンは beta.0 リリース直後の [2a2f9a9a196cf1502c3002f14bd42b13a4dcaa53](https://github.com/angular/angular/commit/2a2f9a9a196cf1502c3002f14bd42b13a4dcaa53) です。基本的に beta.0 としてリリースされたバージョンと同じと考えていいと思います。

## TL;DR

- Angular 2 はプラットフォームごとに様々なコンポーネントを切り替えられるようになっている。
- Component のコンパイルの結果、複数の Command ができる。画面を描画するための命令群のようなもの。
- RuntimeCompiler がトップレベル Component 型を DOM 関係なくコンパイルして ProtoViewRef を返す。View を作るための雛形のようなもの。
- AppViewManagaer が ProtoViewRef（コンパイルされた Component のメタ情報）をもとに Component のインスタンスを作り、DOM要素に紐付ける。

## ソースを読む前に

まず前提として Angular 2 のレポジトリは [GitHub の angular/angular](https://github.com/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 を読んでみる

アプリを初期化する bootstrap から始めましょう。以下に出てくるパスは全て modules 以下の話です。ブラウザ向けの bootstrap のある `angular2/platfrom/browser.ts` を見てみます。

```ts:angular2/platfrom/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` にはトップレベルの Component の型と（必要あれば）独自の providers を渡します。以下のような処理の流れですね。

1. `platform`: `BROWSER_PROVIDERS` から `PlatformRef` を作る。
2. `PlatformRef#application`: `BROWSER_APP_PROVIDERS` + 独自の providers を渡して `ApplicationRef` を作る。
3. `ApplicationRef#bootstrap`: トップレベルの Component の型を渡してアプリケーションを開始する。

ではこれらのメソッドを見ていきましょう。

ちなみに `BROWSER_PROVIDERS` はプラットフォーム共通の provider に DOM アダプタの初期化処理を加えたものの模様。

```ts:angular2/src/platform/browser_common.ts
/**
 * 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}),
]);
```

## platform

import/export を追っていくと `platform` は `angular2/src/core/application_ref.ts` にあることがわかります。ちなみに `platform` の引数は `BROWSER_PROVIDERS` です。ざっくり言うと `_createPlatform` というのを呼んでいるだけです。

```ts:angular2/src/core/application_ref.ts
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_

`PlatformRef_` を見てみましょう。

```ts:angular2/src/core/application_ref.ts
constructor(private _injector: Injector, private _dispose: () => void) { super(); }
```

引数をメンバ変数としてとっておいているだけですね。第二引数は、アプリ終了時の処理のようです。

## PlatformRef_#bootstrap

`bootstrap` 内で呼ばれている `application` メソッドを見てみましょう。ここでいう providers は `BROWSER_APP_PROVIDERS` + 独自 providers です。

```ts:angular2/src/core/application_ref.ts
application(providers: Array<Type | Provider | any[]>): ApplicationRef {
  var app = this._initApp(createNgZone(), providers);
  return app;
}
```

`NgZone` を作って `_initApp` に渡しています。

```ts:angular2/src/core/application_ref.ts
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` が入っている injector を継承して `BROWSER_APP_PROVIDERS` + 独自 providers を足した子 injector が作られているのがわかります。

TODO: NgZone#run とは？

### BROWSER_APP_PROVIDERS

ちなみに `BROWSER_APP_PROVIDERS` は、ブラウザ上で動くアプリに必要な provider たちの模様。`BROWSER_APP_COMMON_PROVIDERS` と `COMPILER_PROVIDERS` が分かれているのは、オフラインコンパイルへの布石？

```angular2/platform/browser.ts
/**
 * 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` は以下のような感じです。

```angular2/src/platform/browser_common.ts
/**
 * 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_

今度は ApplicationRef_ を見てみましょう。

```ts:angular2/src/core/application_ref.ts
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` は

```ts:angular2/src/core/zone/ng_zone.ts
/**
 * 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()` の中では変更検知をキックしているようです。

TODO: Zone.js の詳細

## ApplicationRef_#bootstrap

そして最後の `bootstrap` です。

```ts:angular2/src/core/application_ref.ts
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 _;
  });
}
```

長いですが結局は、さらに子 `injector` を作って、`ComponentRef` なるものを取得し、それを使って `_loadComponent` して、それで結果の `Promise` を `resolve` しているようです。

```ts
_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` はコンパイル済の Component の模様。いつの間にコンパイルしていたのでしょうか？injector から ComponentRef を取得するところ？

`APP_COMPONENT_REF_PROMISE` で検索するとありました。

```ts:angular2/src/core/application_ref.ts
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` がトップレベル `Component` の型をもらって `ComponentRef` を作っているようです。

## DynamicComponentLoader#loadAsRoot

```ts:angular2/src/core/linker/dynamic_component_loader.ts
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);
    });
  }
```

1. Compiler#compileInHost: Component 型をコンパイルして ProtoViewRef を作ります。
2. AppViewManager#createRootHostView: ProtoViewRef から Component のインスタンスを作成し、Global View（ブラウザなら Document）内で Component のセレクタ（上書き可）にマッチする最初の要素に結びつけます。返すのは HostViewRef。
3. AppViewManager#getHostElement:（省略）
4. AppViewManager#getComponent:（省略）

TODO: HostViewRef と ElementRef の違い？後者は DOM 要素を指していそう。前者は？

## Compiler_#compileInHost

いよいよ核心に迫って来ました。

```ts:angular2/src/core/linker/compiler.ts
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` で以下のようになっています。

```ts:angular2/src/compiler/compiler.ts
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 と名前についているということは Runtime じゃない Compiler もあるはずなので、オフライン用の Compiler も用意されるのかもしれません（AngularConnect のキーノートでもオフラインコンパイルがそのうち実装されるようなことを言っていました）。Compiler_ はオフラインでコンパイルしたテンプレートを使うためのもの？

## RuntimeCompiler

気を取り直して RuntimeCompiler を追ってみます。

```ts:angular2/src/compiler/runtime_compiler.ts
compileInHost(componentType: Type): Promise<ProtoViewRef> {
  return this._templateCompiler.compileHostComponentRuntime(componentType)
      .then(compiledHostTemplate => internalCreateProtoView(this, compiledHostTemplate));
}
```

1. `TemplateCompiler#compileHostComponentRuntime`: これがまさに Angular 2 のコンパイルの模様！
2. `internalCreateProtoView`: 辿ってみると `ProtoViewFactory#createHost` を呼んでいます。基本的には 1 でできた `CompiledHostTemplate` を Renderer に登録した後、`AppProtoView` で包んでいるだけのようです。

## TemplateCompiler#compileHostComponentRuntime

TemplateCompiler を見てみます。

```ts:angular2/src/compiler/template_compiler.ts
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));
}
```

1. `RuntimeMetadataResolver#getMetadata`: Component のメタデータを取得していますね。
2. `createHostComponentMeta`:（省略）
3. `TemplateCompiler#_compileComponentRuntime`: ここがメインのコンパイル処理のようです！

## TemplateCompiler#_compileComponentRuntime

なかなかの大物が出て来ました。

```ts:angular2/src/compiler/template_compiler.ts
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` というものを作って返しています。

```ts:angular2/src/core/linker/template_commands.ts
@CONST()
export class CompiledComponentTemplate {
  constructor(public id: string, public changeDetectorFactory: Function,
              public commands: TemplateCmd[], public styles: string[]) {}
}
```

`changeDetectorFactory` の実体, `commands`, `styles` は、`CompiledComponentTemplate` を返してしまった後に埋まるようになっているのが面白いところ。

以下が実際のコンパイルの処理です。

1. `StyleCompiler#compileComponentRuntime`: CSS のパース（多分 AST にしている）
2. `TemplateCompiler#normalizeDirectiveMetadata`: テンプレート内で使われる Directive たち（ngFor など PLATFORM_DIRECTIVES 内のものや directives で指定したもの）の Metadata を正規化
3. `TemplateParser#parse`: 2 を使ってテンプレートをパースして AST に。結果は `TemplateAST[]`。
4. ChangeDetector のファクトリ関数を作成。
5. `TemplateCompiler#_compileCommandsRuntime`: 上で作った AST などの情報から commands（`TemplateCmd[]`） を作る。中では再帰的に子 Component をコンパイルしていくようです。

どうやらここで作りたかったのは `TemplateCmd[]` のようです。

## Command とは一体何？

```ts:angular2/src/core/linker/template_commands.ts
export interface TemplateCmd extends RenderTemplateCmd {
  visit(visitor: RenderCommandVisitor, context: any): any;
}
```

TemplateCmd の親は・・・

```ts:angular2/src/core/render/api.ts
/**
 * 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 というのは Rendering のための命令のことのようです。つまるところ Angular 2 は、Component の Template を一回 AST にパースした後、（ブラウザの場合は）HTML を描画するための命令セットに変換していることがわかりました。このことをコンパイルと言っているようです。

## 終わりに

今回はこのあたりで時間切れですが、（次があれば）次回はコンパイルして作った Command がどうやって使われているかを見ていきたいなと思います。みんなで Angular 2 の内部構造を解析しましょう！