DI 原理解析 並實現一個簡易版 DI 容器
- 2021 年 8 月 26 日
- 筆記
本文基於自身理解進行輸出,目的在於交流學習,如有不對,還望各位看官指出。
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 的單例服務特性)。
相關資料可查看:
//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')
· 往期精彩 ·
【不懂物理的前端不是好的遊戲開發者(一)—— 物理引擎基礎】
【京東購物小程式 | Taro3 項目分包實踐】
歡迎關注凹凸實驗室部落格:aotu.io
或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章: