1

依赖注入是 Angular 的一大特性,通过它你能够写出更加易于维护的代码。但 JavaScript 语言本身并没有提供依赖注入的功能,那么 Angular 是如何实现依赖注入功能的呢?阅读本文,你就能够找到答案了。

一个典型的 Angular 应用程序,从开发者编写的源代码到在浏览器中运行,主要有 2 个关键步骤:

  1. 模板编译,即通过运行 ng build 等构建命令调用编译器编译我们编写的代码。
  2. 运行时运行,模板编译的产物借助运行时代码在浏览器中运行。

首先我们来编写一个简单的 Angular 应用,AppComponent 组件有个依赖项 HeroService:

import { Component } from '@angular/core';
import { Injectable } from '@angular/core';
@Injectable({
  providedIn: 'root'
})
export class HeroService {
  name = 'hero service';
  constructor() { }
}
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  title: string;
  constructor(heroService: HeroService) {
    this.title = heroService.name;
  }
}

上面的代码经过 Angular 编译器编译打包后的产物大概是这样:
wecom-temp-5d3ed0bdb181087c9fd3e963d18b2794.png

由图可知编译的产物主要分为 HeroService 和 AppComponent 两个部分,因为本文主要是讲解依赖注入的实现原理,所以对于编译产物的其他部分不做展开讲解。现在我们来重点关注一下依赖注入相关的代码,其中箭头所示代码:

AppComponent.ɵfac = function AppComponent_Factory(t) {
  return new (t || AppComponent)(i0.ɵɵdirectiveInject(HeroService));
};

AppComponent_Factory 函数负责创建 AppComponent,显而易见依赖项 HeroService 是通过 i0.ɵɵdirectiveInject(HeroService) 创建的。 我们继续来看 i0.ɵɵdirectiveInject 函数做了什么。

function ɵɵdirectiveInject(token, flags = InjectFlags.Default) {
    // 省略无关代码
    ......
    return getOrCreateInjectable(tNode, lView, resolveForwardRef(token), flags);
}

这里我们直接定位到 getOrCreateInjectable 这个函数即可。在继续分析这个函数之前,我们来先看看 lView 这个参数。在 Angular 内部,LView[TView.data](http://TView.data) 是两个很重要的视图数据,Ivy(即 Angular 编译和渲染管道)依据这些内部数据进行模板渲染。LView 被设计成一个单个的数组,通过这个数组包含了模板渲染需要的所有数据。TView.data 的数据可以被所有的模板实例共享。
现在我们回到 getOrCreateInjectable 这个函数:

function getOrCreateInjectable(tNode, lView, token, xxxxx) {
    // 省略无关代码
    ......
    return lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue);
}

返回的是函数 lookupTokenUsingModuleInjector 执行的结果,通过名字大致可以了解到是通过模块注入器来查找对应的 Token:

function lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue) {
   // 省略无关代码
   ......
   return moduleInjector.get(token, notFoundValue, flags & InjectFlags.Optional);
}

moduleInjector.get 方法最终是由 R3Injector 去执行查找:

  this._r3Injector.get(token, notFoundValue, injectFlags);
}

这里我们又引入了一个新的名词:R3Injector。R3Injector 和 NodeInjector 是 Angular 中两种不同类型的注入器。前者是模块层级的注入器,后者则是组件层级的。我们继续看 R3Injector 的 get 方法做了什么吧:

  get(token, notFoundValue = THROW_IF_NOT_FOUND, flags = InjectFlags.Default) {
    // 省略无关代码
    ......
    let record = this.records.get(token);
    if (record === undefined) {
      const def = couldBeInjectableType(token) && getInjectableDef(token);
      if (def && this.injectableDefInScope(def)) {
        record = makeRecord(injectableDefOrInjectorDefFactory(token), NOT_YET);
      }
      else {
        record = null;
      }
        this.records.set(token, record);
    }
    // If a record was found, get the instance for it and return it.
    if (record != null /* NOT null || undefined */) {
      return this.hydrate(token, record);
}

通过上述代码,我们大概可以了解到 R3Injector 的 get 方法的大致流程。this.records 是一个 Map 集合,key 是 token, value 则是 token对应的实例。如果在 Map 集合中没有找到对应的实例,就创建一条记录。get 方法返回的 this.hydrate 函数执行的结果,这个函数最终执行的是本文开头模板编译产物中 HeroService.ɵfac 函数:

  HeroService.ɵfac = function HeroService_Factory(t) {
    return new (t || HeroService)();
  };

至此 Angular 依赖注入的流程就分析完了。本文分析的代码示例使用的是模块注入器,那么组件级别的注入器背后的实现流程是怎样的呢?要使用组件级别的注入器,我们需要在@Component装饰器中显式声明 provider:


@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [{
    provide: HeroService,
    useClass: HeroService
  }]
})

和模块注入器相同的流程就不再赘述,在 getOrCreateInjectable 函数中组件注入器关键函数如下:

function getOrCreateInjectable(tNode, lView, token, xxx) {
    // 省略无关代码
    ......
    const instance = searchTokensOnInjector(injectorIndex, lView, token, xxxx);
  if (instance !== NOT_FOUND) {
    return instance;
  }
}

instance 是由函数 searchTokensOnInjector 创建的:

function searchTokensOnInjector(injectorIndex, lView, token, xxxx) {
  // 省略无关代码
  ......
    return getNodeInjectable(lView, currentTView, injectableIdx, tNode);
}

最终 getNodeInjectable 函数解释了最终结果:

export function getNodeInjectable(
    lView: LView, tView: TView, index: number, tNode: TDirectiveHostNode): any {
  let value = lView[index];
  const tData = tView.data;
    // ........
  if (isFactory(value)) {
    const factory: NodeInjectorFactory = value;
    try {
      value = lView[index] = factory.factory(undefined, tData, lView, tNode);
          // ...........
  return value
}

也就是说最开始我们分析的 i0.ɵɵdirectiveInject(HeroService) 函数创建的值,就是上面代码中的value。value 是由 factory.factory() 函数创建的,而 factory 函数依然是本文开头模板编译产物中 HeroService.ɵfac 函数。可以看到 R3Injector 和 NodeInjector 的区别在于,一个是通过 this.records 存储依赖注入的实例,而 NodeInjector 则是通过 LView 存储这些信息。

本文首发于个人公众号【朱玉洁的博客】,后续将会持续分享前端相关技术文章,欢迎关注。


Zuckjet
437 声望657 粉丝

学如逆水行舟,不进则退。