本文为您带来VSCode的详细教程。 VSCode 中的每个模块都有大量的装饰器,用于装饰模块及其所依赖的模块变量。 详细信息请参考这篇文章。
依赖注入简介
如果有这样一个模块A,其实现依赖于另一个模块B的能力,那么应该如何设计呢? 很简单,我们可以在模块A的构造函数中实例化模块B,这样我们就可以在模块A内部使用模块B的功能了。
class A {
constructor() {
this.b = new B();
}
}
class B {}
const a = new A();
然而,这有两个问题。 一是在模块A实例化的过程中,需要自动实例化模块B,但是如果模块B的依赖发生变化,模块A的构造函数也需要改变typescript 循环依赖,导致代码耦合。
第二个是,在复杂的项目中,当我们实例化模块A时,我们无法确定模块B是否已经实例化,因为它依赖于其他模块,因此模块B可能会被实例化多次。 如果模块B很重或者需要设计成单例,这会导致性能问题。
为此,更好的方法是将所有模块的实例化交给内部框架,由框架统一管理模块的实例化过程,这样就可以解决上述两个问题。
class A {
constructor(private b: B) {
this.b = b;
}
}
class B {}
class C {
constructor(private a: A, private b: B) {
this.b = b;
}
}
const b = new B();
const a = new A(b);
const c = new C(a, b);
这些从外部注入依赖对象以防止模块内部实例化依赖项的方法称为依赖项注入(简称 DI)。 这是软件工程中常见的设计模式。 我们可以在Java的Spring、JS的Angular、Node的NestJS等框架中看到这些设计模式的应用。
事实上,在实际应用中,由于模块较多,依赖关系复杂,我们很难像前面的例子那样规划每个模块的实例化时序,所以我们编译模块的实例化顺序。 而且,很多模块可能不需要第一次创建,需要按需实例化。 因此,粗暴的统一实例化是不可取的。
为此,我们需要一个统一的框架来分析和管理所有模块的实例化过程。 这就是依赖注入框架的作用。
依靠 TypeScript 的装饰器能力,VSCode 实现了一个极其轻量级的依赖注入框架。 我们可以先简单实现一下,来揭开这个巧妙设计的神秘面纱。
最小依赖注入框架设计
实现一个依赖注入框架只需要两步。 一是在框架中声明并注册模块进行管理,二是在模块构造函数中声明需要依赖的模块是什么。
我们首先看一下模块注册过程,这需要TypeScript的类装饰器能力。 注入时,我们只需要判断模块是否已经注册即可。 如果没有,则传入模块id(这里简化为模块类名)并键入,完成单个模块的注册。
export function Injectable(): ClassDecorator {
return (Target: Class): any => {
if (!collection.providers.has(Target.name)) {
collection.providers.set(Target.name, target);
}
return target;
};
}
那么我们来看看模块是如何声明依赖的,这需要TypeScript的属性装饰器能力。 当我们注入时,我们首先判断依赖的模块是否已经被实例化。 如果没有,则依赖模块将被实例化并存储在框架中进行管理。 最后返回已经实例化的模块实例。
export function Inject(): PropertyDecorator {
return (target: Property, propertyKey: string) => {
const instance = collection.dependencies.get(propertyKey);
if (!instance) {
const DependencyProvider: Class = collection.providers.get(propertyKey);
collection.dependencies.set(propertyKey, new DependencyProvider());
}
target[propertyKey] = collection.dependencies.get(propertyKey);
};
}
最后,您只需要确保在项目运行之前框架本身已实例化。 (在示例中表示为注入器)
export class ServiceCollection {
readonly providers = new Map();
readonly dependencies = new Map();
}
const collection = new ServiceCollection();
export default collection;
这样,一个简化的依赖注入框架就完成了。 由于保存了模块的类型和实例,因此实现了模块的按需实例化,无需在项目启动时初始化所有模块。
我们可以尝试调用一下,以其中列出的例子为例:
@injectable()
class A {
constructor(@inject() private b: B) {
this.b = b;
}
}
@injectable()
class B {}
class C {
constructor(@inject() private a: A, @inject() private b: B) {
this.b = b;
}
}
const c = new C();
不需要知道模块A和B的实例化时机,直接初始化任意一个模块即可,框架会手动为你找到并实例化所有依赖的模块。
VSCode的依赖集合实现
它引入了依赖注入框架的最小实现。 但当我们实际阅读VSCode的源码时,发现VSCode中的依赖注入框架似乎并不是这样消耗的。
例如,在下面的轮询服务中,我们发现该类没有@injectable()作为该类的依赖集合,并且依赖服务直接使用其类名作为装饰器而不是@inject()。
// srcvsworkbenchservicesauthenticationbrowserauthenticationService.ts
export class AuthenticationService extends Disposable implements IAuthenticationService {
constructor(
@IActivityService private readonly activityService: IActivityService,
@IExtensionService private readonly extensionService: IExtensionService,
@IStorageService private readonly storageService: IStorageService,
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
@IDialogService private readonly dialogService: IDialogService,
@IQuickInputService private readonly quickInputService: IQuickInputService
) {}
}
虽然这里的修饰符实际上并不是指向类名,而是一个同名的资源描述符id(在VSCode中称为ServiceIdentifier),通常用字符串或Symbol来标记。
使用ServiceIdentifier作为id,而不是简单粗暴的通过类名作为id来注册Service,有助于解决项目中某个接口可能存在多态实现,需要在同一时刻存在同名类的多个实例的问题。同时。
据悉,在构造ServiceIdentifier时,我们可以将类声明注入到框架中,而无需@injectable()显示调用。
那么,这样的ServiceIdentifier应该如何构造呢?
// srcvsplatforminstantiationcommoninstantiation.ts
/**
* The *only* valid way to create a {{ServiceIdentifier}}.
*/
export function createDecorator(serviceId: string): ServiceIdentifier {
if (_util.serviceIds.has(serviceId)) {
return _util.serviceIds.get(serviceId)!;
}
const id = function (target: Function, key: string, index: number): any {
if (arguments.length !== 3) {
throw new Error('@IServiceName-decorator can only be used to decorate a parameter');
}
storeServiceDependency(id, target, index);
};
id.toString = () => serviceId;
_util.serviceIds.set(serviceId, id);
return id;
}
// 被 ServiceIdentifier 装饰的类在运行时,将收集该类的依赖,注入到框架中。
function storeServiceDependency(id: Function, target: Function, index: number): void {
if ((target as any)[_util.DI_TARGET] === target) {
(target as any)[_util.DI_DEPENDENCIES].push({ id, index });
} else {
(target as any)[_util.DI_DEPENDENCIES] = [{ id, index }];
(target as any)[_util.DI_TARGET] = target;
}
}
我们只需要通过createDecorator为类创建一个唯一的ServiceIdentifier,并将其作为修饰符即可。
以里面的AuthenticationService为例。 如果其依赖的ActivityService需要以多态方式实现,则只需要更改ServiceIdentifier修饰符即可确定实现方法,无需修改业务调用代码。
export const IActivityServicePlanA = createDecorator("IActivityServicePlanA");
export const IActivityServicePlanB = createDecorator("IActivityServicePlanB");
export interface IActivityService {...}
export class AuthenticationService {
constructor(
@IActivityServicePlanA private readonly activityService: IActivityService,
) {}
}
循环依赖问题
模块之间的依赖关系可能存在循环依赖,例如A依赖Btypescript 循环依赖,B又依赖A。在这些情况下,两个模块的实例化会导致死循环,所以我们需要添加循环依赖度量框架的机制来避免它。
本质上,健康的模块依赖是有向无环图(DAG)。 前面我们介绍了DAG在Excel表函数中的应用,在依赖注入框架的设计中也同样适用。
我们可以通过深度优先搜索(DFS)来衡量模块之间的依赖关系,如果发现循环依赖则抛出异常。
// src/vs/platform/instantiation/common/instantiationService.ts
while (true) {
let roots = graph.roots();
// if there is no more roots but still
// nodes in the graph we have a cycle
if (roots.length === 0) {
if (graph.length !== 0) {
throwCycleError();
}
break;
}
for (let root of roots) {
// create instance and overwrite the service collections
const instance = this._createInstance(root.data.desc, []);
this._services.set(root.data.id, instance);
graph.removeNode(root.data);
}
}
该方法通过获取图节点的出度,将类的所有依赖关系提取为根,然后一一实例化,一路剥离依赖节点。 由于依赖树是逐层构建的,因此可以按顺序实例化。 当发现该类的所有依赖都被实例化后,图中仍然存在节点,则认为存在循环依赖,并抛出异常。