博客 / 詳情

返回

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

在上一篇介紹了 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

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.