Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【Javascript】IOC/DI原理分析,并实现一个 简易版 IOC/DI 框架 #44

Open
AwesomeDevin opened this issue Apr 9, 2021 · 0 comments

Comments

@AwesomeDevin
Copy link
Owner

AwesomeDevin commented Apr 9, 2021

本文基于自身理解进行输出,目的在于交流学习,如有不对,还望各位看官指出。

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')
@AwesomeDevin AwesomeDevin changed the title 【javascript】IOC/DI原理分析,并实现一个 简易版 IOD/DI 框架 【Javascript】IOC/DI原理分析,并实现一个 简易版 IOD/DI 框架 Apr 12, 2021
@AwesomeDevin AwesomeDevin changed the title 【Javascript】IOC/DI原理分析,并实现一个 简易版 IOD/DI 框架 【Javascript】IOC/DI原理分析,并实现一个 简易版 IOC/DI 框架 Apr 12, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant