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

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. ESP32-默认事件循环

    默认的事件循环是一个事件循环的系统应用发布和处理事件(例如,Wi-Fi无线事件). 基于ESP-IDF4.1 1 #include "esp_log.h" 2 #include & ...

  2. 关于varnish缓存

    目录 缓存的概念 一.varnish缓存 1. 简介 2. 总体结构 2.1 两个主进程 2.1.1 Management进程 2.1.2 Child/Cacher进程 2.2 Varnish的日志收 ...

  3. nacos集群部署

    对于nacos的集群部署,建议大家参考nacos官网进行,地址:https://nacos.io/zh-cn/docs/cluster-mode-quick-start.html 下面是我自己部署na ...

  4. Quick BI的复杂系统为例:那些年,我们一起做过的性能优化

    背景 一直以来,性能都是技术层面不可避开的话题,尤其在中大型复杂项目中.犹如汽车整车性能,追求极速的同时,还要保障舒适性和实用性,而在汽车制造的每个环节.零件整合情况.发动机调校等等,都会最终影响用户 ...

  5. modelsim 独立仿真vivado的IP核及仿真脚本

    Modelsim独立仿真vivado的IP 最近一直在做local dimming项目的FPGA硬件实现,算法的其中一步就是直方图统计,即数字图像的某一灰度级的像素数,这个直方图的源码找了半天才搞到, ...

  6. artTemplate学习

    参考:https://www.2cto.com/kf/201711/699818.html 参考:https://blog.csdn.net/ruisenLi/article/details/8841 ...

  7. Java多线程(上)

    Java多线程 程序.进程和线程 一.程序 程序是存储在磁盘上, 包含可执行机器指令和数据的静态实体. 即进程或者任务是处于活动状态的计算机程序. 二.进程 进程是资源(CPU.内存等)分配的基本单位 ...

  8. 在NestJS 中添加对Stripe 的WebHook 验证

    在NestJS 中添加对Stripe 的WebHook 验证 背景介绍 Nest 是一个用于构建高效,可扩展的NodeJS 服务器端应用程序的框架.它使用渐进式JavaScript, 内置并完全支持T ...

  9. VM虚拟机桥接模式无法联网、NAT模式能正常联网

    桥接模式:使虚拟机客户机可以和主机在同一网段,这样,和主机同局域网内的其他主机就也可以ping到虚拟机了: 因此,虚拟机设置为桥接模式,这样以后就可以方便的使用虚拟机了: 有时,虚拟机为桥接模式上不了 ...

  10. 网络损伤仪WANsim的带宽限制功能

    带宽限制功能 带宽限制功能是网络损伤仪WANsim的第一项损伤功能.进入WANsim的报文首先会经过报文过滤器的处理,随后,就会进入带宽限制. 点击虚拟链路,就可以进入网络损伤界面,对报文进行带宽限制 ...