乐趣区

关于javascript: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),不定时推送文章。

退出移动版