DI 原理解析 並實現一個簡易版 DI 容器

本文基於自身理解進行輸出,目的在於交流學習,如有不對,還望各位看官指出。

DI

DI—Dependency Injection,即“依賴注入”:對象之間依賴關係由容器在運行期決定,形象的說,即由容器動態的將某個對象注入到對象屬性之中。依賴注入的目的並非爲軟件系統帶來更多功能,而是爲了提升對象重用的頻率,併爲系統搭建一個靈活、可擴展的框架。

使用方式

首先看一下常用依賴注入 (DI)的方式:

function Inject(target: any, key: string){
    target[key] = new (Reflect.getMetadata('design:type',target,key))()
}

class A {
    sayHello(){
        console.log('hello')
    }
}

class B {
    @Inject   // 編譯後等同於執行了 @Reflect.metadata("design:type", A)
    a: A

    say(){
       this.a.sayHello()  // 不需要再對class A進行實例化
    }
}

new B().say() // hello

原理分析

TS在編譯裝飾器的時候,會通過執行__metadata函數多返回一個屬性裝飾器@Reflect.metadata,它的目的是將需要實例化的service以元數據'design:type'存入reflect.metadata,以便我們在需要依賴注入時,通過Reflect.getMetadata獲取到對應的service, 並進行實例化賦值給需要的屬性。

@Inject編譯後代碼:

var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};

// 由於__decorate是從右到左執行,因此, defineMetaData 會優先執行。
__decorate([
    Inject,
    __metadata("design:type", A)  //  作用等同於 Reflect.metadata("design:type", A)
], B.prototype, "a", void 0);

即默認執行了以下代碼:

Reflect.defineMetadata("design:type", A, B.prototype, 'a');

Inject函數需要做的就是從metadata中獲取對應的構造函數並構造實例對象賦值給當前裝飾的屬性

function Inject(target: any, key: string){
    target[key] = new (Reflect.getMetadata('design:type',target,key))()
}

不過該依賴注入方式存在一個問題:

  • 由於Inject函數在代碼編譯階段便會執行,將導致B.prototype在代碼編譯階段被修改,這違反了六大設計原則之開閉原則(避免直接修改類,而應該在類上進行擴展)
    那麼該如何解決這個問題呢,我們可以借鑑一下TypeDI的思想。

typedi

typedi 是一款支持TypeScript和JavaScript依賴注入工具
typedi 的依賴注入思想是類似的,不過多維護了一個container

1. metadata

在瞭解其container前,我們需要先了解 typedi 中定義的metadata,這裏重點講述一下我所瞭解的比較重要的幾個屬性。

  • id: service的唯一標識
  • type: 保存service構造函數
  • value: 緩存service對應的實例化對象
const newMetadata: ServiceMetadata<T> = {
      id: ((serviceOptions as any).id || (serviceOptions as any).type) as ServiceIdentifier,    // service的唯一標識
      type: (serviceOptions as ServiceMetadata<T>).type || null,  // service 構造函數
      value: (serviceOptions as ServiceMetadata<T>).value || EMPTY_VALUE,  // 緩存service對應的實例化對象
};

2. container 作用

function ContainerInstance() {
        this.metadataMap = new Map();  //保存metadata映射關係,作用類似於Refect.metadata
        this.handlers = []; // 事件待處理隊列
        get(){};  // 獲取依賴注入後的實例化對象
         ...
}
  • this. metadataMap - @service會將service構造函數以metadata形式保存到this.metadataMap中。
    • 緩存實例化對象,保證單例;
  • this.handlers - @inject會將依賴注入操作的對象目標行爲以 object 形式 push 進 handlers 待處理數組。
    • 保存構造函數靜態類型屬性間的映射關係。
{
        object: target,  // 當前等待掛載的類的原型對象
        propertyName: propertyName,  // 目標屬性值
        index: index, 
        value: function (containerInstance) {   // 行爲
            var identifier = Reflect.getMetadata('design:type', target, propertyName)
            return containerInstance.get(identifier);
        }
}

@inject將該對象 push 進一個等待執行的 handlers 待處理數組裏,當需要用到對應 service 時執行 value函數 並修改 propertyName。

if (handler.propertyName) {
     instance[handler.propertyName] = handler.value(this);
}
  • get - 對象實例化操作及依賴注入操作
    • 避免直接修改類,而是對其實例化對象的屬性進行拓展;

相關結論

  • typedi中的實例化操作不會立即執行, 而是在一個handlers待處理數組,等待Container.get(B),先對B進行實例化,然後從handlers待處理數組取出對應的value函數並執行修改實例化對象的屬性值,這樣不會影響Class B 自身
  • 實例的屬性值被修改後,將被緩存到metadata.value(typedi 的單例服務特性)。

相關資料可查看:

https://stackoverflow.com/questions/55684776/typedi-inject-doesnt-work-but-container-get-does

new B().say()  // 將會輸出sayHello is undefined

Container.get(B).say()  // hello word

實現一個簡易版 DI Container

此處代碼依賴TS,不支持JS環境

interface Handles {
    target: any
    key: string,
    value: any
}

interface Con {
    handles: Handles []   // handlers待處理數組
    services: any[]  // service數組,保存已實例化的對象
    get<T>(service: new () => T) : T   // 依賴注入並返回實例化對象
    findService<T>(service: new () => T) : T  // 檢查緩存
    has<T>(service: new () => T) : boolean  // 判斷服務是否已經註冊
}

var container: Con = {
    handles: [],  // handlers待處理數組
    services: [], // service數組,保存已實例化的對象
    get(service){
        let res: any = this.findService(service)
        if(res){
            return  res
        }

        res = new service()
        this.services.push(res)
        this.handles.forEach(handle=>{
            if(handle.target !== service.prototype){
                return
            }
            res[handle.key] = handle.value
        })
        return res
    },

    findService(service){
        return this.services.find(instance => instance instanceof service)
    },

   // service是否已被註冊
    has(service){
        return !!this.findService(service)
    }
}

function Inject(target: any, key: string){
    const service = Reflect.getMetadata('design:type',target,key)
    
    // 將實例化賦值操作緩存到handles數組
    container.handles.push({
        target,
        key,
        value: new service()
    })

    // target[key] = new (Reflect.getMetadata('design:type',target,key))()
}

class A {
    sayA(name: string){
        console.log('i am '+ name)
    }
}

class B {
    @Inject
    a: A

    sayB(name: string){
       this.a.sayA(name)
    }
}

class C{
    @Inject
    c: A

    sayC(name: string){
       this.c.sayA(name)
    }
}

// new B().sayB(). // Cannot read property 'sayA' of undefined
container.get(B).sayB('B')
container.get(C).sayC('C')

· 往期精彩 ·

【不懂物理的前端不是好的遊戲開發者(一)—— 物理引擎基礎】

【3D性能優化 | 說一說glTF文件壓縮】

【京東購物小程序 | Taro3 項目分包實踐】

歡迎關注凹凸實驗室博客:aotu.io

或者關注凹凸實驗室公衆號(AOTULabs),不定時推送文章:

歡迎關注凹凸實驗室公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章