likes
comments
collection
share

从nest.js中了解IoC和DI的实现

作者站长头像
站长
· 阅读数 13

做一个有温度和有干货的技术分享作者 —— Qborfy

背景

从上一篇《从egg.js转到nest.js》,想对其再深入了解一下,尤其比较好奇Typescript是如何实现IoCDI,因为在Java的是通过的反射(Spring IoC实现原理)去创建对应的类。因此下文将详细讲解Nest.js中IoC和DI的实现原理。

前置知识

在了解实现原理之前有几个知识概念,需要了解一下:

  • IoC和DI
  • JavaScript的Reflect
  • TypeScript的装饰器

IoC和DI

IoC和DI其实同属于一个技术理念,下面维基百科的介绍:

IoC,控制反转(英语:Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。

简单的说IoC是一个开发代码的设计原则,DI则是实现这个设计原则的方案。

IoC

从代码层上来讲解IoC,简单的说就是:

  • Class A中用到了Class B的对象b,一般情况下,需要在A的代码中显式地用 new 创建 B 的对象。
  • 使用IoC设计原则后,A 的代码只需要定义一个 private 的B对象,不需要直接 new 来获得这个对象,而是通过相关的容器控制程序来将B对象在外部new出来并注入到A类里的引用中。
  • IoC将采用依赖注入或依赖查找两种方案去实现

再通俗一点,就是有一个IoC容器管家,负责你开发的代码类的归置,你只管使用代码类,不用管它放在哪里,只需要调用即可。

DI

DI,Dependency Injection,依赖注入

依赖注入是被动的接收对象,在类A的实例创建过程中即创建了依赖的B对象,通过类型或名称来判断将不同的对象注入到不同的属性中 依赖查找是主动索取相应类型的对象,获得依赖对象的时间也可以在代码中自由控制

简单的说,就是依赖注入是将需要注入的对象完全交给框架去实现,而依赖查找则是开发者通过框架提供的方法,由自己控制需要注入的时间点。

问题

采用IoC和DI,需要注意的问题是:

  • 循环依赖,就是A依赖B,B依赖A,如何避免这种情况发生,或者框架提供什么样的方案去避免?
  • 如果依赖的类越来越多,会不会导致项目启动速度变慢,因为需要初始化类很多,尤其当遇到一些类初始化可能会错误,但其实是可以忽略的?
  • 初始化类的顺序如何控制,如:A依赖B,需要B实例化后才能实例?

JavaScript的Reflect

Reflect在MDN网站是这么解释的:

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers (en-US)的方法相同。Reflect不是一个函数对象,因此它是不可构造的。 其中的一些方法与 Object 相同,尽管二者之间存在某些细微上的差别。

按照前端开发者理解来说,Reflect能解决开发中遇到很多this的代理问题,虽然大部分方案都可以通过其他方式解决,但是Reflect的定义能帮助我们快速实现这些功能。

Reflect符合ES6标准的提供的API有如下几个:

  • Reflect.apply(target, thisArgument, argumentsList),和 Function.prototype.apply(thisArgument, argumentsList) 功能类似,也是调用函数,且允许将函数的this指向thisArgument
  • Reflect.construct(target, argumentsList[, newTarget]),new一个target,且可以将target的this的指向新的newTarget对象
  • Reflect.defineProperty(target, propertyKey, attributes),拦截target对象的操作,和 Object.defineProperty() 类似
  • Reflect.deleteProperty(target, propertyKey),作为函数的delete操作符,相当于执行 delete target[name]。
  • Reflect.get(target, propertyKey[, receiver]),获取target的属性值,和target[name]的区别在于可以receiver,可以指定调用属性值的时候this
  • Reflect.getOwnPropertyDescriptor(target, propertyKey),类似于 Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符,否则返回 undefined。
  • Reflect.getPrototypeOf(target),返回指定对象的原型(即内部的 [[Prototype]] 属性的值)
  • Reflect.has(target, propertyKey),判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同
  • Reflect.ownKeys(target),返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受enumerable 影响).
  • Reflect.isExtensible(target), 判断一个对象是否可扩展(即是否能够添加新的属性)
  • Reflect.preventExtensions(target),阻止新属性添加到对象
  • Reflect.set(target, propertyKey, value[, receiver]),将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
  • Reflect.setPrototypeOf(target, prototype),可设置对象的原型,即内部的 [[Prototype]] 属性)为另一个对象或 null,利用原型链用来强制给某个对象增加额外方法

当然还有一些没有进入标准,但是在ES7提案的方法Reflect Metadata(Typescript已实现),后面Nest.js已采用的方法,主要有以下几个:

  • Reflect.getMetadata(metadataKey, target, propertyKey), 用于获取某个类的元数据
  • Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);, 用于设置某个类的元数据

简单理解这个api方法,你可以通过Reflect.defineMetadata获取到类或者函数的参数类型,也可以给类或者函数设置元数据再获取,具体代码如下:

function Prop(): PropertyDecorator {
  return (target, key: string) => {
    const type = Reflect.getMetadata('design:type', target, key);
    console.log(`${key} type: ${type.name}`);
    // other...
  };
}

class SomeClass {
  @Prop()
  public Aprop!: string;
}

TypeScript 的优势了,TypeScript 支持编译时自动添加一些 metadata 数据,如下所示:

  • Reflect.getMetadata("design:type", target, key), 获取target函数类型
  • Reflect.getMetadata("design:paramtypes", target, key), 获取target函数参数类型
  • Reflect.getMetadata("design:returntype", target, key), 获取target函数返回值类型

这个Reflect.getMetadata("design:paramtypes", target, key)基本上就是Nest.js实现Ioc和DI的核心代码。

TypeScript的装饰器

装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。 装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。

如何实现一个装饰器呢?

如果我们要定制一个修饰器如何应用到一个声明上,我们得写一个装饰器工厂函数。 装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。

function color(value: string) { // 这是一个装饰器工厂
    return function (target) { //  这是装饰器
        // do something with "target" and "value"...
    }
}

@color('blue')
function say(){
    ....
}

IoC和DI实现原理

其实在了解完Reflect.getMetadata,我们就大概知道IoC和DI的实现原理,我们以一个@Controller为例, 具体步骤如下:

  • 实现@Controller装饰器工厂,标识待注入的类
  • 实现IoC容器,注册要被依赖注入的类
  • 获取待注入的类其构造函数所需要的参数类型,并实例化,返回待注入的类

具体代码如下:

// 实现Controller装饰器
function Controller(path: string){
    return function(target){
        Reflect.defineMetadata('Controller', path, target);
    }
}

// 需要依赖注入的类
class A(){
    say(){
        console.log('aaaaaa');
    }
}

// 引用
@Controller("/api")
class Demo(){
    construtor(a: A ){
        this.a = A;
    }

    say(){
        this.a.say();
    }
}


// 实现Ioc容器和DI依赖注入
class Container {
  provides = new Map()
  
  // 注册要被依赖注入类,形成IoC容器 后续可以做
  addProvide(provider) {
    this.provides.set(provider.name, provider)
  }
  // 注入依赖类
  inject(target) {
      // 获取参数类型
      const paramTypes = Reflect.getMetadata('design:paramtypes', target) || []
      const args = paramTypes.map((type) => {
        return new type() // 简单做一下实例化
      })
      return Reflect.construct(target, args)
  }
}

const container = new Container()
const project = container.inject(Project)

// project就是最终生成返回使用的类

project.say(); // 输出 aaaaaa

所以Nest.js实现IoC和DI的核心实现原理:

  • 通过装饰器给 class 或者对象添加 metadata
  • 运行的时候通过这些元数据来实现依赖的扫描,对象的创建等等功能

当然,还有很多问题没解决,目前只是简单实现了依赖注入,上述IoC的问题还没有解决,由于篇幅较长,所以拆成几篇,放到后续继续研究。