聊聊Angular 依賴注入體系中的基本概念

2022-03-03 13:00:23
本篇文章帶大家聊聊,介紹一下依賴注入的基本概念,希望對大家有所幫助!

作為「為大型前端專案」而設計的前端框架,Angular 其實有許多值得參考和學習的設計,本系列主要用於研究這些設計和功能的實現原理。本文主要圍繞 Angular 中的最大特點——依賴注入,首先來介紹一些 Angular 依賴注入體系中的基本概念。

依賴注入

既然要介紹 Angular 框架的依賴注入設計,那麼先鋪墊一下依賴注入的基本概念。我們常常會搞混依賴倒置原則(DIP)、控制反轉(IoC)、依賴注入(DI)這幾個概念,因此這裡會先簡單介紹一下。【相關教學推薦:《》】

依賴倒置原則、控制反轉、依賴注入

低耦合、高內聚大概是每個系統的設計目標之一,而為此產生了很多的設計模式和理念,其中便包括依賴倒置原則、控制反轉的設計思想。

(1) 依賴倒置原則(DIP)。

依賴倒置原則的原始定義為:

  • 高層模組不應該依賴低層模組,兩者都應該依賴其抽象;
  • 抽象不應該依賴細節,細節應該依賴抽象。

簡單說便是:模組間不應該直接依賴對方,應該依賴一個抽象的規則(介面或者時抽象類)。

(2) 控制反轉(IoC)。

控制反轉的定義為:模組間的依賴關係從程式內部提到外部來範例化管理。即物件在被建立的時候,由一個調控系統內所有物件的外界實體控制,並將其所依賴的物件的參照傳遞(注入)給它。

實現控制反轉主要有兩種方式:

  • 依賴注入:被動的接收依賴物件
  • 依賴查詢:主動索取依賴的物件

(3) 依賴注入。

依賴注入,是控制反轉的最為常見的一種技術。

依賴倒置和控制反轉兩者相輔相成,常常可以一起使用,可有效地降低模組間的耦合。

Angular 中的依賴注入

在 Angular 中,同樣使用了依賴注入的技術,DI 框架會在範例化某個類時,向其提供這個類所宣告的依賴項(依賴項:指當類需要執行其功能時,所需要的服務或物件)。

Angular 中的依賴注入基本上是圍繞著元件或者是模組展開的,主要用於給新建的元件提供依賴。

Angular 中主要的依賴注入機制是注入器機制

  • 應用中所需的任何依賴,都必須使用該應用的注入器來註冊一個提供者,以便注入器可以使用這個提供者來建立新範例
  • Angular 會在啟動過程中,建立全應用級注入器以及所需的其它注入器

這裡面主要涉及兩個概念,分別是Injector 注入器Provider 提供商,我們來看看。

Injector 注入器

Injector 注入器用於建立依賴,會維護一個容器來管理這些依賴,並儘可能地複用它們。注入器會提供依賴的一個單例,並把這個單例物件注入到多個元件中。

顯然,作為一個用來建立、管理、維護依賴的容器,注入器的功能很簡單:建立依賴範例、獲取依賴範例、管理依賴範例。我們也可以從抽象類Injector的原始碼中看出來:

export abstract class Injector {
  // 找不到依賴
  static THROW_IF_NOT_FOUND = THROW_IF_NOT_FOUND;
  // NullInjector 是樹的頂部
  // 如果你在樹中向上走了很遠,以至於要在 NullInjector 中尋找服務,那麼將收到錯誤訊息,或者對於 @Optional(),返回 null
  static NULL: Injector = new NullInjector();

  // 根據提供的 Token 從 Injector 檢索範例
  abstract get<T>(
    token: Type<T> | AbstractType<T> | InjectionToken<T>,
    notFoundValue?: T,
    flags?: InjectFlags
  ): T;

  // 建立一個新的 Injector 範例,該範例提供一個或多個依賴項
  static create(options: {
    providers: StaticProvider[];
    parent?: Injector;
    name?: string;
  }): Injector;

  // ɵɵdefineInjectable 用於構造一個 InjectableDef
  // 它定義 DI 系統將如何構造 Token,並且在哪些 Injector 中可用
  static ɵprov = ɵɵdefineInjectable({
    token: Injector,
    providedIn: "any" as any,
    // ɵɵinject 生成的指令:從當前活動的 Injector 注入 Token
    factory: () => ɵɵinject(INJECTOR),
  });

  static __NG_ELEMENT_ID__ = InjectorMarkers.Injector;
}

也就是說,我們可以將需要共用的依賴範例新增到注入器中,並通過 Token 查詢和檢索注入器來獲取相應的依賴範例。

需要注意的是,Angular 中的注入器是分層的,因此查詢依賴的過程也是向上遍歷注入器樹的過程。

這是因為在 Angular 中,應用是以模組的方式組織的,具體可以參考5.模組化組織篇。一般來說,頁面的 DOM 是以html作為根節點的樹狀結構,以此為基礎,Angular 應用中的元件和模組也是與之相伴的樹狀結構。

而注入器服務於元件和模組,同樣是掛載與模組和組織上的樹狀結構。因此,Injector 也劃分為模組和元件級別,可分別為元件和模組提供依賴的具體範例。注入器是可繼承的,這意味著如果指定的注入器無法解析某個依賴,它就會請求父注入器來解析它,我們同樣可以從上面的建立注入器程式碼中看到:

// 建立一個新的 Injector 範例,可傳入 parent 父注入器
static create(options: {providers: StaticProvider[], parent?: Injector, name?: string}): Injector;

在某個注入器的範圍內,服務是單例的。也就是說,在指定的注入器中最多隻有某個服務的最多一個範例。如果不希望在所有地方都使用該服務的同一個範例,則可以通過註冊多個注入器、並按照需要關聯到元件和模組中的方式,來按需共用某個服務依賴的範例。

我們可以看到建立一個新的Injector範例時,傳入的引數包括Provider,這是因為Injector不會直接建立依賴,而是通過Provider來完成的。每個注入器會維護一個提供者的列表,並根據元件或其它服務的需要,用它們來提供服務的範例。

Provider 提供者

Provider 提供者用來告訴注入器應該如何獲取或建立依賴,要想讓注入器能夠建立服務(或提供其它型別的依賴),必須使用某個提供者設定好注入器。

一個提供者物件定義瞭如何獲取與 DI 令牌(token) 相關聯的可注入依賴,而注入器會使用這個提供者來建立它所依賴的那些類的範例。

關於 DI 令牌:

  • 當使用提供者設定注入器時,就會把提供者和一個 DI 令牌關聯起來;
  • 注入器維護一個內部令牌-提供者的對映表,當請求一個依賴項時就會參照它,令牌就是這個對映表的鍵。

提供者的型別很多,從官方檔案中可以閱讀它們的具體定義:

export type Provider =
  | TypeProvider
  | ValueProvider
  | ClassProvider
  | ConstructorProvider
  | ExistingProvider
  | FactoryProvider
  | any[];

提供者的解析過程如下:

function resolveReflectiveFactory(
  provider: NormalizedProvider
): ResolvedReflectiveFactory {
  let factoryFn: Function;
  let resolvedDeps: ReflectiveDependency[];
  if (provider.useClass) {
    // 使用類來提供依賴
    const useClass = resolveForwardRef(provider.useClass);
    factoryFn = reflector.factory(useClass);
    resolvedDeps = _dependenciesFor(useClass);
  } else if (provider.useExisting) {
    // 使用已有依賴
    factoryFn = (aliasInstance: any) => aliasInstance;
    // 從根據 token 獲取具體的依賴
    resolvedDeps = [
      ReflectiveDependency.fromKey(ReflectiveKey.get(provider.useExisting)),
    ];
  } else if (provider.useFactory) {
    // 使用工廠方法提供依賴
    factoryFn = provider.useFactory;
    resolvedDeps = constructDependencies(provider.useFactory, provider.deps);
  } else {
    // 使用提供者具體的值作為依賴
    factoryFn = () => provider.useValue;
    resolvedDeps = _EMPTY_LIST;
  }
  //
  return new ResolvedReflectiveFactory(factoryFn, resolvedDeps);
}

根據不同型別的提供者,通過解析之後,得到由注入器 Injector 使用的提供者的內部解析表示形式:

export interface ResolvedReflectiveProvider {
  // 鍵,包括系統範圍內的唯一 id,以及一個 token
  key: ReflectiveKey;
  // 可以返回由鍵表示的物件的範例的工廠函數
  resolvedFactories: ResolvedReflectiveFactory[];
  // 指示提供者是多提供者,還是常規提供者
  multiProvider: boolean;
}

提供者可以是服務類ClassProvider本身,如果把服務類指定為提供者令牌,那麼注入器的預設行為是用new來範例化那個類。

Angular 中的依賴注入服務

在 Angular 中,服務就是一個帶有@Injectable裝飾器的類,它封裝了可以在應用程式中複用的非 UI 邏輯和程式碼。Angular 把元件和服務分開,是為了增進模組化程度和可複用性。

@Injectable標記一個類,以確保編譯器將在注入類時生成必要的後設資料(後設資料在 Angular 中也是很重要的一部分),以建立類的依賴項。

@Injectable裝飾器的類會在編譯之後,得到 Angular 可注入物件:

// 根據其 Injectable 後設資料,編譯 Angular 可注入物件,並對結果進行修補
export function compileInjectable(type: Type<any>, srcMeta?: Injectable): void {
  // 該編譯過程依賴 @angular/compiler
  // 可參考編譯器中的 compileFactoryFunction compileInjectable 實現
}

Angular 中可注入物件(InjectableDef)定義 DI 系統將如何構造 token 令牌,以及在哪些注入器(如果有)中可用:

export interface ɵɵInjectableDef<T> {
  // 指定給定型別屬於特定注入器,包括 root/platform/any/null 以及特定的 NgModule
  providedIn: InjectorType<any> | "root" | "platform" | "any" | null;
  // 此定義所屬的令牌
  token: unknown;
  // 要執行以建立可注入範例的工廠方法
  factory: (t?: Type<any>) => T;
  // 在沒有顯式注入器的情況下,儲存可注入範例的位置
  value: T | undefined;
}

使用@Injectable()providedIn時,優化工具可以進行 Tree-shaking 優化,從而刪除應用程式中未使用的服務,以減小捆綁包尺寸。

總結

本文簡單介紹了在 Angular 依賴注入體系中比較關鍵的幾個概念,主要包括InjectorProviderInjectable

對於注入器、提供者和可注入服務,我們可以簡單地這樣理解:

  • 注入器用於建立依賴,會維護一個容器來管理這些依賴,並儘可能地複用它們。

  • 一個注入器中的依賴服務,只有一個範例。

  • 注入器需要使用提供者來管理依賴,並通過 token(DI 令牌)來進行關聯。

  • 提供者用於高速注入器應該如何獲取或建立依賴。

  • 可注入服務類會根據後設資料編譯後,得到可注入物件,該物件可用於建立範例。

更多程式設計相關知識,請存取:!!

以上就是聊聊Angular 依賴注入體系中的基本概念的詳細內容,更多請關注TW511.COM其它相關文章!