【DI 原理解析 并实现一个简易版 DI 容器】本文基于自身理解进行输出 , 目的在于交流学习 , 如有不对 , 还望各位看官指出 。DIDI—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: Asay(){this.a.sayHello()// 不需要再对class A进行实例化}}new B().say() // hello原理分析TS在编译装饰器的时候 , 会通过执行__metadata函数多返回一个属性装饰器@Reflect.metadata , 它的目的是将需要实例化的service以元数据'design:type'存入reflect.metadata , 以便我们在需要依赖注入时 , 通过Reflect.getMetadata获取到对应的service , 并进行实例化赋值给需要的属性 。
@Inject编译后代码:
var __metadata = https://tazarkount.com/read/(this && this.__metadata) || function (k, v) {if (typeof Reflect ==="object" && typeof Reflect.metadata =https://tazarkount.com/read/=="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 的依赖注入思想是类似的 , 不过多维护了一个
container1. 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.metadatathis.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 undefinedContainer.get(B).say()// hello word实现一个简易版 DI Container此处代码依赖TS,不支持JS环境interface Handles {target: anykey: 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){returnres}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 {@Injecta: AsayB(name: string){this.a.sayA(name)}}class C{@Injectc: AsayC(name: string){this.c.sayA(name)}}// new B().sayB(). // Cannot read property 'sayA' of undefinedcontainer.get(B).sayB('B')container.get(C).sayC('C')· 往期精彩 ·【不懂物理的前端不是好的游戏开发者(一)—— 物理引擎基础】
【3D性能优化 | 说一说glTF文件压缩】
【京东购物小程序 | Taro3 项目分包实践】欢迎关注凹凸实验室博客:aotu.io
或者关注凹凸实验室公众号(AOTULabs) , 不定时推送文章:

文章插图
- 春季老年人吃什么养肝?土豆、米饭换着吃
- 三八妇女节节日祝福分享 三八妇女节节日语录
- 老人谨慎!选好你的“第三只脚”
- 校方进行了深刻的反思 青岛一大学生坠亡校方整改校规
- 脸皮厚的人长寿!有这特征的老人最长寿
- 长寿秘诀:记住这10大妙招 100%增寿
- 春季老年人心血管病高发 3条保命要诀
- 眼睛花不花要看四十八 老年人怎样延缓老花眼
- 香槟然能防治老年痴呆症? 一天三杯它人到90不痴呆
- 老人手抖的原因 为什么老人手会抖
