背景
前端发展至今已经过去30余年,前端应用领域在不断壮大的过程中,也变得越来越复杂,随着代码行数和项目需求的增加,内部模块间的依赖可能也会随之越来越复杂,模块间的 低复用性 导致应用 难以维护,不过我们可以借助计算机领域的一些优秀的编程理念来一定程度上解决这些问题,接下来要讲述的 IoC
就是其中之一。
什么是IOC
其实学过java的就一定会知道java中有一个非常著名的框架叫做springboot,它就是将AOP和IOC等概念运用到了极致的代表作,那么具体IOC是做什么的呢,我们可以看下下面一段描述。
IoC
的全称叫做 Inversion of Control
,可翻译为为「控制反转」或「依赖倒置」,它主要包含了三个准则:
- 高层次的模块不应该依赖于低层次的模块,它们都应该依赖于抽象
- 抽象不应该依赖于具体实现,具体实现应该依赖于抽象
- 面向接口编程 而不要面向实现编程
假设我们有一个类Human
,要实例一个Human
,我们需要实例一个类Clothes
。而实例化衣服Clothes
,我们又需要实例化布Cloth
,实例化纽扣等等。
当需求达到一定复杂的程度时,我们不能为了一个人穿衣服去从布从纽扣开始从头实现,最好能把所有的需求放到一个工厂或者是仓库,我们需要什么直接从工厂的仓库里面直接拿。
这个时候就需要依赖注入了,我们实现一个IOC容器(仓库),然后需要衣服就从仓库里面直接拿实例好的衣服给人作为属性穿上去。
这也就大大减少了我们编码的成本。
如何实现一个IOC
其实实现IOC的思路很简单,或者说这是一个很轻的东西,任何人只要知道原理都能去实现它。首先我们重复下刚刚所描述的ioc的概念,在正常情况下我们需要Human,Clothes类的时候都只能一个一个新建。
export class Human {}
export class Clothes {}
function test() {
const human = new Human();
const clothes = new Clothes();
}
我们不难看出少量的对象需要新建的时候这么做确实没啥问题,但是如果在一个庞大系统中存在上百上千个对象,我们在不同业务场景又需要load不同的对象,同时我们还需要控制对象销毁避免GC。这样来说我们想要处理好前端对象我们得做很多工作,这样我们就引出了接下来我们需要做的工作如何去管理对象。
第一步:实现一个容器
容器其实是一个高大上的概念,其实简单来说就是个Map对象之类的东西,用于存放现有的对象。下面是我具体实现的一个小demo,主要是存放的容器类。为了保证容器唯一,所以我将其设计成了单例模式。
export class SimpleContainer {
private containerMap = new Map<string | symbol, any>();
private static _instance: SimpleContainer;
public set(id: string | symbol, value: any): void {
this.containerMap.set(id, value);
}
public get<T extends any>(id: string | symbol): T {
return this.containerMap.get(id) as T;
}
public has(id: string | symbol): Boolean{
return this.containerMap.has(id);
}
public remove(id: string | symbol): void {
if (this.containerMap.has(id)) {
this.containerMap.delete(id);
}
}
public static getInstance(): SimpleContainer {
if(!this._instance) {
this._instance = new SimpleContainer();
}
return this._instance;
}
public get container(): SimpleContainer {
return SimpleContainer._instance;
}
}
第二步:用好装饰器
随着TypeScript和ES6里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。 装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。 Javascript里的装饰器目前处在 建议征集的第二阶段,但在TypeScript里已做为一项实验性特性予以支持。
注意 装饰器是一项实验性特性,在未来的版本中可能会发生改变。
如果需要使用装饰器,我们得在tsconfig.json中配置experimentalDecorators为true开启支持。
首先我们先看下我们需要实现的最后效果
@Service('human')
export class Human {}
@Service('clothes')
export class Clothes {}
export class Test {
@Inject()
private human!: Human;
}
我们需要通过Service注入需要实例化的类,然后再通过Inject在外面需要的对象中注入进去,这就是装饰器在IOC中所发挥的作用。
那么Service是如何实现的呢?
export function Service(idOrSingleton?: string | boolean, singleton?: boolean): Function {
return (target: ConstructableFunction) => {
let id;
let singleton;
const container = SimpleContainer.getInstance();
// 代码逻辑复杂有所删减
container.set(id, singleInstance || new target());
};
};
我们所有的实例初始化都在Service中实现也就是这么一个句话,container.set(id, singleInstance || new target());。
export function Inject(value?: string): PropertyDecorator {
return (target: any, propertyKey: string | symbol) => {
const id = value || propertyKey;
const container = SimpleContainer.getInstance();
const _dependency = container.get(id) ? container.get(id) : null;
if (_dependency) {
target[propertyKey] = _dependency;
}
return target;
};
}
通过Inject来实现对象的实例话和返还,所利用的特性也是PropertyDecorator所支持的能够对参数赋值的能力。识别到对应装饰器的对象的时候,我们通过属性装饰器来进行赋值和初始化。
这里需要补充一下装饰器的相关知识。
1.装饰器对类的行为改变是在编译时,而非在运行时。
2.装饰器运行顺序,并非按照类,属性,方法来进行的,我们在使用的时候需要注意,我这里的顺序是:属性->类->方法。
第三步:使用容器
我们又回到了第二步的最初,当我们实现了Inject和Service装饰器之后我们就可以快乐的初始化了。
@Inject()
private human!: Human;
通过如上操作之后我们就可以使用该对象的内容了。
扩展和展望
回到我们实现IOC的初衷,我们希望通过某种技术来管理我们繁乱的对象和代码,所以我们才做了这么一个容器,当然现在这个容器还十分简陋,依然还有很多可以扩展的空间,比如说:关于对象的生命周期的控制,如何更加友好的使用容器中的对象。
最后
一个小广告,欢迎使用基于上述代码所开发的ioc包,目前还能简陋,不过笔者会迅速强化和迭代它。
easy-ts-di:https://github.com/guanjiangtao/easy-ts-di
欢迎大佬们可以提供意见,钢筋走开~~~~