在上一篇介绍了 VSCode 的依赖注入设计,并且实现了一个简略的 IOC 框架。然而间隔成为一个生产环境可用的框架还差的很远。
行业内曾经有许多十分优良的开源 IOC 框架,它们划分了更为清晰地模块来应答简单状况下依赖注入运行的正确性。
这里我将以 InversifyJS 为例,剖析它的生命周期设计,来弄清楚在一个优良的 IOC 框架中,实现一次注入流程到底是什么样的。
InversifyJS 的生命周期
在激活 InversifyJS 后,框架通常会监听并经验五个阶段,别离是:
- Annotation 正文阶段
- Planning 布局阶段
- Middleware (optional) 中间件钩子
- Resolution 解析执行阶段
- Activation (optional) 激活钩子
本篇文章将着重介绍其中的 三个必选阶段。旨在解释框架到底是如何布局模块实例化的先后顺序,以实现依赖注入能力的。
接下来的解析将围绕如下例子:
@injectable()
class FooBar implements FooBarInterface {
public foo : FooInterface;
public bar : BarInterface;
constructor(@inject("FooInterface") foo: FooInterface,
@inject("BarInterface") bar: BarInterface
) {
this.foo = foo;
this.bar = bar;
}
}
const container = new Container();
const foobar = container.get<FooBarInterface>("FooBarInterface");
Annotation 正文阶段
在此阶段中,框架将通过装璜器为所有接入框架的对象打上标记,以便布局阶段时进行治理。
在这个阶段中,最重要的 API 就是 injectable
。它应用 Reflect metadata,对 Class 构造函数中通过 inject
API 注入的 property 进行标注,并挂在在了该类的 metadataKey
上。
function injectable() {return function(target: any) {if (Reflect.hasOwnMetadata(METADATA_KEY.PARAM_TYPES, target)) {throw new Error(ERRORS_MSGS.DUPLICATED_INJECTABLE_DECORATOR);
}
const types = Reflect.getMetadata(METADATA_KEY.DESIGN_PARAM_TYPES, target) || [];
Reflect.defineMetadata(METADATA_KEY.PARAM_TYPES, types, target);
return target;
};
}
Planning 布局阶段
本阶段时该框架的外围阶段,它真正生成了在一个 Container 中,所有类模块的依赖关系树。因而,在 Container 类进行实例化时,布局阶段就开始了。
在实例化时,依据传入的 id 与 scope 能够确定该实例容器的作用域范畴,生成一个 context,领有对内左右模块的管理权。
class Context implements interfaces.Context {
public id: number;
public container: interfaces.Container;
public plan: interfaces.Plan;
public currentRequest: interfaces.Request;
public constructor(container: interfaces.Container) {this.id = id(); // generate a unique id
this.container = container;
}
public addPlan(plan: interfaces.Plan) {this.plan = plan;}
public setCurrentRequest(currentRequest: interfaces.Request) {this.currentRequest = currentRequest;}
}
咱们能够留神到,这个 context 中蕴含一个空的 plan 对象,这是 planning 阶段的外围,该阶段就是为生成的容器布局好要执行的工作。
plan 对象中将蕴含一个 request 对象,request 是一个可递归的属性构造,它蕴含了要查找的 id 外,还须要 target 参数,即规定找到依赖实例后将援用赋值给哪个参数。
class Request implements interfaces.Request {
public id: number;
public serviceIdentifier: interfaces.ServiceIdentifier<any>; // 被润饰类 id
public parentContext: interfaces.Context;
public parentRequest: interfaces.Request | null; // 树形构造的 request,指向父节点
public bindings: interfaces.Binding<any>[];
public childRequests: interfaces.Request[]; // 树形构造的 request,指向子节点
public target: interfaces.Target; // 指向赋值指标参数
public requestScope: interfaces.RequestScope;
...
}
以篇头的例子为例。在容器执行 get 函数后,框架生成了一个新的 plan,该 plan 的生成过程中将执行_createSubRequests 办法,从上而下创立 Request 依赖树。
创立实现后的 plan 对象生成的 request 树将蕴含有申请指标为 null 的根 request 与两个子 request:
第一个子 request 指向 FooInterface 接口,并且申请后果的 target 赋值给构造函数中的参数 foo。第二个子 request 指向 BarInterface 接口,并且申请后果的 target 赋值给构造函数中的参数 bar。
留神,此处的依赖树生成仍在 interface 层面,没有任何类被实例化。
用一张图来更直观地体现该阶段中各对象的生成调用过程:
这样,每一个类与其依赖项之间的申请关系就结构结束了。
Resolution 解析执行阶段
该阶段便是执行在布局阶段中生成的 request 依赖树,从无依赖的叶子节点开始,自下而上实例化每一个依赖类,到根 request 完结时,即最终实现 FooBar
本身的实例化。
且该解析过程能够抉择同步或异步执行,在简单状况下,应用异步懒加载的形式执行解析,有助于进步性能。
至此,一次残缺的具备依赖的类的实例化就实现了。咱们能够通过打印依赖树,清晰地察看到该实例依赖了哪些实例,从而防止了所有可能的循环依赖,与屡次结构依赖带来的内存泄露等很多难以排查的问题。
参考资料
InversifyJS Architecture Overview