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

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

DI 原理解析 并实现一个简易版 DI 容器的更多相关文章

  1. 依赖注入[5]: 创建一个简易版的DI框架[下篇]

    为了让读者朋友们能够对.NET Core DI框架的实现原理具有一个深刻而认识,我们采用与之类似的设计构架了一个名为Cat的DI框架.在<依赖注入[4]: 创建一个简易版的DI框架[上篇]> ...

  2. 依赖注入[4]: 创建一个简易版的DI框架[上篇]

    本系列文章旨在剖析.NET Core的依赖注入框架的实现原理,到目前为止我们通过三篇文章(<控制反转>.<基于IoC的设计模式>和< 依赖注入模式>)从纯理论的角度 ...

  3. .NET CORE学习笔记系列(2)——依赖注入[4]: 创建一个简易版的DI框架[上篇]

    原文https://www.cnblogs.com/artech/p/net-core-di-04.html 本系列文章旨在剖析.NET Core的依赖注入框架的实现原理,到目前为止我们通过三篇文章从 ...

  4. .NET Core的文件系统[5]:扩展文件系统构建一个简易版“云盘”

    FileProvider构建了一个抽象文件系统,作为它的两个具体实现,PhysicalFileProvider和EmbeddedFileProvider则分别为我们构建了一个物理文件系统和程序集内嵌文 ...

  5. tomcat原理解析(一):一个简单的实现

    tomcat原理解析(一):一个简单的实现 https://blog.csdn.net/qiangcai/article/details/60583330 2017年03月07日 09:54:27 逆 ...

  6. 手动实现一个简易版SpringMvc

    版权声明:本篇博客大部分代码引用于公众号:java团长,我只是在作者基础上稍微修改一些内容,内容仅供学习与参考 前言:目前mvc框架经过大浪淘沙,由最初的struts1到struts2,到目前的主流框 ...

  7. 如何实现一个简易版的 Spring - 如何实现 Setter 注入

    前言 之前在 上篇 提到过会实现一个简易版的 IoC 和 AOP,今天它终于来了...相信对于使用 Java 开发语言的朋友们都使用过或者听说过 Spring 这个开发框架,绝大部分的企业级开发中都离 ...

  8. 如何实现一个简易版的 Spring - 如何实现 Constructor 注入

    前言 本文是「如何实现一个简易版的 Spring」系列的第二篇,在 第一篇 介绍了如何实现一个基于 XML 的简单 Setter 注入,这篇来看看要如何去实现一个简单的 Constructor 注入功 ...

  9. 如何实现一个简易版的 Spring - 如何实现 @Component 注解

    前言 前面两篇文章(如何实现一个简易版的 Spring - 如何实现 Setter 注入.如何实现一个简易版的 Spring - 如何实现 Constructor 注入)介绍的都是基于 XML 配置文 ...

随机推荐

  1. mysql Authentication plugin 'caching_sha2_password' is not supported问题处理

    使用mysql8.0版本,登录失败,提示 Authentication plugin 'caching_sha2_password' is not supported. 原因是在MySQL 8.0以后 ...

  2. Java | 字符串缓冲区(StringBuilder)

    为什么要出现字符缓冲区 我们都知道,String类是不可变的,但是有的时候,我们要用到字符串的拼接,如果拼接的数量小的时候,还可以,但是如果拼接的数据量太大的话,内存的占用就太大了,所以这个时候再用S ...

  3. spring中如何向一个单例bean中注入非单例bean

    看到这个题目相信很多小伙伴都是懵懵的,平时我们的做法大都是下面的操作 @Component public class People{ @Autowired private Man man; } 这里如 ...

  4. shell脚本编写规范和相关变量类型

    shell编程规范与变量    一.shell脚本概述    ① 什么是shell? Linux中有哪些shell?    ② shell的作用   ③ 用户的登录shell    ④ shell脚本 ...

  5. EXCEL中的多个条件同时成立写法

    =IF(AND($B2>0,$C2>0,$D2>0,$E2>0),(($B2*1000/$C2/60/$D2)*$E2),0)点击F2,粘贴上边的公式选择F2到f200ctrl ...

  6. python numpy 求数组的百位分数

    百分位数,统计学术语,如果将一组数据从小到大排序,并计算相应的累计百分位,则某一百分位所对应数据的值就称为这一百分位的百分位数.运用在教育统计学中,例如表现测验成绩时,称PR值.分位数是以概率将一批数 ...

  7. vue2.x移动端ui框架选型

    前言 最近公司准备做移动端spa项目,需要选一个ui框架.优先考虑谷歌Material Design设计风格.针对市面上的框架进行了一次调研,简单总结如下. 选型原则:1. 优先考虑md风格. 2. ...

  8. Maven BOM!拿来吧你

    what BOM? BOM(Bill of Materials)是由Maven提供的功能,它通过定义一整套相互兼容的jar包版本集合, 使用时只需要依赖该BOM文件,即可放心的使用需要的依赖jar包, ...

  9. P5110 块速递推-光速幂、斐波那契数列通项

    P5110 块速递推 题意 多次询问,求数列 \[a_i=\begin{cases}233a_{i-1}+666a_{i-2} & i>1\\ 0 & i=0\\ 1 & ...

  10. Maven作用及应用

    1.简介 Maven是一个项目管理的Java 工具,在JavaEE中,我们可以使用Maven方便地管理团队合作的项目,现在我们在学习JavaEE框架,使用Maven可以管理类库,有效方便地供团队中的其 ...