阅读 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 中,我们传递一个顶层组件的类型以及(如果需要的话)自定义的提供程序。以下是处理流程的示例。

    1. 平台:通过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);
    });
  }
    1. 编译器#在主机上编译Component类型并创建ProtoViewRef。

 

    1. AppViewManager#创建根主机视图:从ProtoViewRef中创建Component实例,并将其与在全局视图(如浏览器的文档)中与Component选择器(可覆盖)匹配的第一个元素相关联。返回的是HostViewRef。

 

    1. 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));
}
    1. 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));
}
    1. 获取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[]) {}
}

改变检测器工厂的实体、命令和样式在返回编译组件模板后会被填充,这是一个有趣的地方。

以下是实际的编译处理过程。

    1. 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的内部结构吧!

bannerAds