Angular 依賴注入原理

Zuckjet 發表於 2022-01-25
Angular

依賴注入是 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 儲存這些資訊。

本文首發於個人公眾號【朱玉潔的部落格】,後續將會持續分享前端相關技術文章,歡迎關注。