Angular|Angular 依赖注入机制实现原理的深入介绍

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { Routes, RouterModule } from '@angular/router'; import { HttpModule } from '@angular/http'; import { AppComponent } from './app.component'; export const ROUTER_CONFIG: Routes = [ { path: '', loadChildren: './home/home.module#HomeModule' }, { path: 'about', loadChildren: './about/about.module#AboutModule' }, { path: 'contact', loadChildren: './contact/contact.module#ContactModule' }, ]; @NgModule({ imports: [BrowserModule, HttpModule, RouterModule.forRoot(ROUTER_CONFIG)], bootstrap: [AppComponent], declarations: [AppComponent], }) export class AppModule {}

上面的实例代码,定义了一个根组件和一些路由规则,通过这些规则,路由到不同的模块。
Angular 将生成对 VM(虚拟机)友好的代码,以尽可能提高其高性能。
Angular 将为我们的每个模块(module)生成一个注入器 Injector,因此在我们的例子中,它将采用 AppModule(我们的装饰类)并创建一个名为 AppModuleInjector 的注入器。
下面是编译器生成的 AppModule Injector 代码:
import { NgModuleInjector } from '@angular/core/src/linker/ng_module_factory'; import { CommonModule } from '@angular/common/src/common_module'; import { ApplicationModule, _localeFactory } from '@angular/core/src/application_module'; import { BrowserModule, errorHandler } from '@angular/platform-browser/src/browser'; import { RouterModule, ROUTER_FORROOT_GUARD } from '@angular/router/src/router_module'; import { NgLocaleLocalization, NgLocalization } from '@angular/common/src/localization'; import { ApplicationInitStatus, APP_INITIALIZER } from '@angular/core/src/application_init'; import { Testability, TestabilityRegistry } from '@angular/core/src/testability/testability'; import { HttpModule } from '@angular/http/src/http_module'; import { ApplicationRef, ApplicationRef_ } from '@angular/core/src/application_ref'; import { BrowserModule } from '@angular/platform-browser/src/browser'; import { Injector } from '@angular/core/src/di/injector'; import { LOCALE_ID } from '@angular/core/src/i18n/tokens'; import { RouterModule, provideForRootGuard } from '@angular/router/src/router_module'; import { Router } from '@angular/router/src/router'; import { NgZone } from '@angular/core/src/zone/ng_zone'; import { Console } from '@angular/core/src/console'; import { ROUTES } from '@angular/router/src/router_config_loader'; import { ErrorHandler } from '@angular/core/src/error_handler'; import { AppModule } from './app.module'; import { AppComponentNgFactory } from './app.component.ngfactory'; class AppModuleInjector extends NgModuleInjector { _CommonModule_0: CommonModule; _ApplicationModule_1: ApplicationModule; _BrowserModule_2: BrowserModule; _ROUTER_FORROOT_GUARD_3: any; _RouterModule_4: RouterModule; _HttpModule_5: HttpModule; _AppModule_6: AppModule; _ErrorHandler_7: any; _ApplicationInitStatus_8: ApplicationInitStatus; _Testability_9: Testability; _ApplicationRef__10: ApplicationRef_; __ApplicationRef_11: any; __ROUTES_12: any[]; constructor(parent: Injector) { super(parent, [AppComponentNgFactory], [AppComponentNgFactory]); }get _ApplicationRef_11(): any { if (this.__ApplicationRef_11 == null) { this.__ApplicationRef_11 = this._ApplicationRef__10; } return this.__ApplicationRef_11; }get _ROUTES_12(): any[] { if (this.__ROUTES_12 == null) { this.__ROUTES_12 = [[ { path: '', loadChildren: './home/home.module#HomeModule' }, { path: 'about', loadChildren: './about/about.module#AboutModule' }, { path: 'contact', loadChildren: './contact/contact.module#ContactModule' } ]]; } return this.__ROUTES_12; }createInternal(): AppModule { this._CommonModule_0 = new CommonModule(); this._ApplicationModule_1 = new ApplicationModule(); this._BrowserModule_2 = new BrowserModule(this.parent.get(BrowserModule, (null as any))); this._ROUTER_FORROOT_GUARD_3 = provideForRootGuard(this.parent.get(Router, (null as any))); this._RouterModule_4 = new RouterModule(this._ROUTER_FORROOT_GUARD_3); this._HttpModule_5 = new HttpModule(); this._AppModule_6 = new AppModule(); this._ErrorHandler_7 = errorHandler(); this._ApplicationInitStatus_8 = new ApplicationInitStatus(this.parent.get(APP_INITIALIZER, (null as any))); this._Testability_9 = new Testability(this.parent.get(NgZone)); this._ApplicationRef__10 = new ApplicationRef_( this.parent.get(NgZone), this.parent.get(Console), this, this._ErrorHandler_7, this, this._ApplicationInitStatus_8, this.parent.get(TestabilityRegistry, (null as any)), this._Testability_9 ); return this._AppModule_6; }getInternal(token: any, notFoundResult: any): any { if (token === CommonModule) { return this._CommonModule_0; } if (token === ApplicationModule) { return this._ApplicationModule_1; } if (token === BrowserModule) { return this._BrowserModule_2; } if (token === ROUTER_FORROOT_GUARD) { return this._ROUTER_FORROOT_GUARD_3; } if (token === RouterModule) { return this._RouterModule_4; } if (token === HttpModule) { return this._HttpModule_5; } if (token === AppModule) { return this._AppModule_6; } if (token === ErrorHandler) { return this._ErrorHandler_7; } if (token === ApplicationInitStatus) { return this._ApplicationInitStatus_8; } if (token === Testability) { return this._Testability_9; } if (token === ApplicationRef_) { return this._ApplicationRef__10; } if (token === ApplicationRef) { return this._ApplicationRef_11; } if (token === ROUTES) { return this._ROUTES_12; }return notFoundResult; }destroyInternal(): void { this._ApplicationRef__10.ngOnDestroy(); } }

为了便于阅读,上面所有的 import 代码,已经手动导入更改为命名导入。在实际生成的代码中,每个模块都使用通配符导入以避免命名冲突。
例如, HttpModule 会像这样被导入:
import * as import6 from '@angular/http/src/http_module';

然后使用 import6.HttpModule 而不是 HttpModule 来引用它。
我们会从上述生成代码中学习三个知识点:类的属性、模块导入以及依赖注入机制的工作方式。
AppModuleInjector properties 在 AppModuleInjector 上为每个提供者/依赖项创建属性:
// ... class AppModuleInjector extends NgModuleInjector { _CommonModule_0: CommonModule; _ApplicationModule_1: ApplicationModule; _BrowserModule_2: BrowserModule; // ... }

这是上面编译输出的一个片段——所以我们将关注定义在类上的三个属性:
  • CommonModule
  • ApplicationModule
  • BrowserModule
我们的模块只声明了 BrowserModule,那么 CommonModule 和 ApplicationModule 是从哪里来的呢? 这些实际上是由 BrowserModule 为我们导出的,所以我们不需要自己导入它们。
模块中每个属性的末尾还附加了一个数字。 就像使用通配符导入一样,这是为了避免提供者之间潜在的命名冲突。
我们可以导入两个使用具有共享名称的服务的模块并且没有递增的数字,它们都将被分配给相同的属性,这可能会导致进一步的错误。
Module imports 编译时,Angular 使用它导入的每个提供程序的直接路径,例如,当我们编写以下代码时:
import { CommonModule } from '@angular/common';

【Angular|Angular 依赖注入机制实现原理的深入介绍】编译后的代码实际上为:
import * as import5 from '@angular/common/src/common_module';

因此,当代码被编译并捆绑在一起时,我们可以利用 tree-shaking 并且只包含我们实际使用的每个模块的部分。
Dependency Injection 每个模块处理自己的依赖注入,如果它没有依赖,则转到父模块,直到找到或未找到它,后者情况下我们会得到一个错误。
重要的是要注意,所有依赖项都使用令牌来唯一标识它们,无论是在注册时还是在查找时。
有两种不同的方式来启动我们的依赖项,要么在 createInternal 中,要么作为属性的 getter。
例如,我们使用 BrowserModule 和 HttpModule,它们是在这里创建的:
class AppModuleInjector extends NgModuleInjector { _CommonModule_0: CommonModule; _ApplicationModule_1: ApplicationModule; _BrowserModule_2: BrowserModule; _HttpModule_5: HttpModule; _AppModule_6: AppModule; createInternal(): AppModule { this._CommonModule_0 = new CommonModule(); this._ApplicationModule_1 = new ApplicationModule(); this._BrowserModule_2 = new BrowserModule(this.parent.get(BrowserModule, (null as any))); this._HttpModule_5 = new HttpModule(); this._AppModule_6 = new AppModule(); // ... return this._AppModule_6; } }

您可以看到 BrowserModule 的两个导出 - CommonModule 和 ApplicationModule 已创建,以及我们其他导入的模块。 我们的实际模块也被创建(AppModule),因此它可以被其他模块使用。
对于所有其他提供者,它们是在需要时通过类中的 getter 创建的。 这是为了避免在不需要时创建提供程序的实例,同时提高初始渲染性能。
每当我们谈到 Angular 中的注入器时,它指的是从我们的模块中生成(编译)的代码。
当 Angular 查找依赖项(例如我们通过构造函数注入的依赖项)时,它会在模块注入器中查找,如果找不到则向上遍历父模块。 如果它不存在,将会抛出一个错误。
当我们在构造函数中使用类型定义时,Angular 使用这些类型(即类)作为查找依赖项的标记。 然后将该令牌传递给 getInternal 并返回依赖项的实例(如果存在)。再次通过源代码来学习:
class AppModuleInjector extends NgModuleInjector { // new BrowserModule(this.parent.get(BrowserModule, (null as any))); _BrowserModule_2: BrowserModule; // new HttpModule() _HttpModule_5: HttpModule; // new AppModule() _AppModule_6: AppModule; getInternal(token: any, notFoundResult: any): any { if (token === BrowserModule) { return this._BrowserModule_2; } if (token === HttpModule) { return this._HttpModule_5; } if (token === AppModule) { return this._AppModule_6; }return notFoundResult; } }

因此,在 getInternal 方法中,可以看到 Angular 正在使用简单的 if 语句检查我们的令牌,并将返回提供者的相关属性 - 如果找到的话。
否则,我们将使用返回的 notFoundResult 来拯救 getInternal 方法。 当 Angular 遍历我们的模块以找到所需的依赖项时,这个 notFoundResult 将为空 - 直到它找到依赖项,或者到达根模块但仍然找不到它,此时将会抛出一个错误。

    推荐阅读