淺析依賴注入框架的生命週期(以 InversifyJS 為例)

Duang發表於2023-02-11

在上一篇介紹了 VSCode 的依賴注入設計,並且實現了一個簡單的 IOC 框架。但是距離成為一個生產環境可用的框架還差的很遠。

行業內已經有許多非常優秀的開源 IOC 框架,它們劃分了更為清晰地模組來應對複雜情況下依賴注入執行的正確性。

這裡我將以 InversifyJS 為例,分析它的生命週期設計,來弄清楚在一個優秀的 IOC 框架中,完成一次注入流程到底是什麼樣的。

InversifyJS 的生命週期

在啟用 InversifyJS 後,框架通常會監聽並經歷五個階段,分別是:

  1. Annotation 註釋階段
  2. Planning 規劃階段
  3. Middleware (optional) 中介軟體鉤子
  4. Resolution 解析執行階段
  5. Activation (optional) 啟用鉤子

本篇文章將著重介紹其中的三個必選階段。旨在解釋框架到底是如何規劃模組例項化的先後順序,以實現依賴注入能力的。

接下來的解析將圍繞如下例子:

    @injectable()
    class FooBar implements FooBarInterface {
        public foo : FooInterface;
        public bar : BarInterface;
        constructor(
            @inject("FooInterface") foo: FooInterface, 
            @inject("BarInterface") bar: BarInterface
        ) {
            this.foo = foo;
            this.bar = bar;
        }
    }
    const container = new Container();
    const foobar = container.get<FooBarInterface>("FooBarInterface");

Annotation 註釋階段

在此階段中,框架將透過裝飾器為所有接入框架的物件打上標記,以便規劃階段時進行管理。

在這個階段中,最重要的 API 就是 injectable 。它使用 Reflect metadata,對 Class 建構函式中透過 inject API 注入的 property 進行標註,並掛在在了該類的 metadataKey 上。

function injectable() {
  return function(target: any) {
    if (Reflect.hasOwnMetadata(METADATA_KEY.PARAM_TYPES, target)) {
      throw new Error(ERRORS_MSGS.DUPLICATED_INJECTABLE_DECORATOR);
    }

    const types = Reflect.getMetadata(METADATA_KEY.DESIGN_PARAM_TYPES, target) || [];
    Reflect.defineMetadata(METADATA_KEY.PARAM_TYPES, types, target);

    return target;
  };
}

Planning 規劃階段

本階段時該框架的核心階段,它真正生成了在一個 Container 中,所有類模組的依賴關係樹。因此,在 Container 類進行例項化時,規劃階段就開始了。

在例項化時,根據傳入的 id 與 scope 可以確定該例項容器的作用域範圍,生成一個 context,擁有對內左右模組的管理權。

class Context implements interfaces.Context {
    public id: number;
    public container: interfaces.Container;
    public plan: interfaces.Plan;
    public currentRequest: interfaces.Request;
    public constructor(
        container: interfaces.Container) {
        this.id = id(); // generate a unique id
        this.container = container;
    }
    public addPlan(plan: interfaces.Plan) {
        this.plan = plan;
    }
    public setCurrentRequest(currentRequest: interfaces.Request) {
        this.currentRequest = currentRequest;
    }
}

我們可以注意到,這個 context 中包含一個空的 plan 物件,這是 planning 階段的核心,該階段就是為生成的容器規劃好要執行的任務。

plan 物件中將包含一個 request 物件,request 是一個可遞迴的屬性結構,它包含了要查詢的 id 外,還需要 target 引數,即規定找到依賴例項後將引用賦值給哪個引數。

class Request implements interfaces.Request {
    public id: number;
    public serviceIdentifier: interfaces.ServiceIdentifier<any>; // 被修飾類 id
    public parentContext: interfaces.Context;
    public parentRequest: interfaces.Request | null; // 樹形結構的 request,指向父節點
    public bindings: interfaces.Binding<any>[];
    public childRequests: interfaces.Request[]; // 樹形結構的 request,指向子節點
    public target: interfaces.Target; // 指向賦值目標引數
    public requestScope: interfaces.RequestScope;
    ...
}

以篇頭的例子為例。在容器執行 get 函式後,框架生成了一個新的 plan,該 plan 的生成過程中將執行_createSubRequests 方法,從上而下建立 Request 依賴樹。

建立完成後的 plan 物件生成的 request 樹將包含有請求目標為 null 的根 request 與兩個子 request:

第一個子 request 指向 FooInterface 介面,並且請求結果的 target 賦值給建構函式中的引數 foo。第二個子 request 指向 BarInterface 介面,並且請求結果的 target 賦值給建構函式中的引數 bar。

注意,此處的依賴樹生成仍在 interface 層面,沒有任何類被例項化。

用一張圖來更直觀地表現該階段中各物件的生成呼叫過程:

20230209165944

這樣,每一個類與其依賴項之間的請求關係就構造完畢了。

Resolution 解析執行階段

該階段便是執行在規劃階段中生成的 request 依賴樹,從無依賴的葉子節點開始,自下而上例項化每一個依賴類,到根 request 結束時,即最終完成 FooBar 自身的例項化。

且該解析過程可以選擇同步或非同步執行,在複雜情況下,使用非同步懶載入的方式執行解析,有助於提高效能。

至此,一次完整的具有依賴的類的例項化就完成了。我們可以透過列印依賴樹,清晰地觀察到該例項依賴了哪些例項,從而避免了一切可能的迴圈依賴,與多次構造依賴帶來的記憶體洩露等很多難以排查的問題。

參考資料

InversifyJS Architecture Overview

相關文章