本文基於自身理解進行輸出,目的在於交流學習,如有不對,還望各位看官指出。
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')
· 往期精彩 ·
【不懂物理的前端不是好的遊戲開發者(一)—— 物理引擎基礎】
【京東購物小程式 | Taro3 專案分包實踐】
歡迎關注凹凸實驗室部落格:aotu.io
或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章: