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

DI

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

使用方式

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

  1. function Inject(target: any, key: string){
  2. target[key] = new (Reflect.getMetadata('design:type',target,key))()
  3. }
  4. class A {
  5. sayHello(){
  6. console.log('hello')
  7. }
  8. }
  9. class B {
  10. @Inject // 編譯後等同於執行了 @Reflect.metadata("design:type", A)
  11. a: A
  12. say(){
  13. this.a.sayHello() // 不需要再對class A進行例項化
  14. }
  15. }
  16. new B().say() // hello

原理分析

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

@Inject編譯後代碼:

  1. var __metadata = (this && this.__metadata) || function (k, v) {
  2. if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
  3. };
  4. // 由於__decorate是從右到左執行,因此, defineMetaData 會優先執行。
  5. __decorate([
  6. Inject,
  7. __metadata("design:type", A) // 作用等同於 Reflect.metadata("design:type", A)
  8. ], B.prototype, "a", void 0);

即預設執行了以下程式碼:

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

Inject函式需要做的就是從metadata中獲取對應的建構函式並構造例項物件賦值給當前裝飾的屬性

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

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

  • 由於Inject函式在程式碼編譯階段便會執行,將導致B.prototype在程式碼編譯階段被修改,這違反了六大設計原則之開閉原則(避免直接修改類,而應該在類上進行擴充套件)

    那麼該如何解決這個問題呢,我們可以借鑑一下TypeDI的思想。

typedi

typedi 是一款支援TypeScript和JavaScript依賴注入工具

typedi 的依賴注入思想是類似的,不過多維護了一個container

1. metadata

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

  • id: service的唯一標識
  • type: 儲存service建構函式
  • value: 快取service對應的例項化物件
  1. const newMetadata: ServiceMetadata<T> = {
  2. id: ((serviceOptions as any).id || (serviceOptions as any).type) as ServiceIdentifier, // service的唯一標識
  3. type: (serviceOptions as ServiceMetadata<T>).type || null, // service 建構函式
  4. value: (serviceOptions as ServiceMetadata<T>).value || EMPTY_VALUE, // 快取service對應的例項化物件
  5. };

2. container 作用

  1. function ContainerInstance() {
  2. this.metadataMap = new Map(); //儲存metadata對映關係,作用類似於Refect.metadata
  3. this.handlers = []; // 事件待處理佇列
  4. get(){}; // 獲取依賴注入後的例項化物件
  5. ...
  6. }
  • this. metadataMap - @service會將service建構函式以metadata形式儲存到this.metadataMap中。

    • 快取例項化物件,保證單例;
  • this.handlers - @inject會將依賴注入操作的物件目標行為以 object 形式 push 進 handlers 待處理陣列。
    • 儲存建構函式靜態型別屬性間的對映關係。
  1. {
  2. object: target, // 當前等待掛載的類的原型物件
  3. propertyName: propertyName, // 目標屬性值
  4. index: index,
  5. value: function (containerInstance) { // 行為
  6. var identifier = Reflect.getMetadata('design:type', target, propertyName)
  7. return containerInstance.get(identifier);
  8. }
  9. }

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

  1. if (handler.propertyName) {
  2. instance[handler.propertyName] = handler.value(this);
  3. }
  • 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

  1. new B().say() // 將會輸出sayHello is undefined
  2. Container.get(B).say() // hello word

實現一個簡易版 DI Container

此處程式碼依賴TS,不支援JS環境

  1. interface Handles {
  2. target: any
  3. key: string,
  4. value: any
  5. }
  6. interface Con {
  7. handles: Handles [] // handlers待處理陣列
  8. services: any[] // service陣列,儲存已例項化的物件
  9. get<T>(service: new () => T) : T // 依賴注入並返回例項化物件
  10. findService<T>(service: new () => T) : T // 檢查快取
  11. has<T>(service: new () => T) : boolean // 判斷服務是否已經註冊
  12. }
  13. var container: Con = {
  14. handles: [], // handlers待處理陣列
  15. services: [], // service陣列,儲存已例項化的物件
  16. get(service){
  17. let res: any = this.findService(service)
  18. if(res){
  19. return res
  20. }
  21. res = new service()
  22. this.services.push(res)
  23. this.handles.forEach(handle=>{
  24. if(handle.target !== service.prototype){
  25. return
  26. }
  27. res[handle.key] = handle.value
  28. })
  29. return res
  30. },
  31. findService(service){
  32. return this.services.find(instance => instance instanceof service)
  33. },
  34. // service是否已被註冊
  35. has(service){
  36. return !!this.findService(service)
  37. }
  38. }
  39. function Inject(target: any, key: string){
  40. const service = Reflect.getMetadata('design:type',target,key)
  41. // 將例項化賦值操作快取到handles陣列
  42. container.handles.push({
  43. target,
  44. key,
  45. value: new service()
  46. })
  47. // target[key] = new (Reflect.getMetadata('design:type',target,key))()
  48. }
  49. class A {
  50. sayA(name: string){
  51. console.log('i am '+ name)
  52. }
  53. }
  54. class B {
  55. @Inject
  56. a: A
  57. sayB(name: string){
  58. this.a.sayA(name)
  59. }
  60. }
  61. class C{
  62. @Inject
  63. c: A
  64. sayC(name: string){
  65. this.c.sayA(name)
  66. }
  67. }
  68. // new B().sayB(). // Cannot read property 'sayA' of undefined
  69. container.get(B).sayB('B')
  70. container.get(C).sayC('C')

· 往期精彩 ·

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

【3D效能優化 | 說一說glTF檔案壓縮】

【京東購物小程式 | Taro3 專案分包實踐】

歡迎關注凹凸實驗室部落格:aotu.io

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