vscode 依赖注入

Jul 7, 2021

前言

在 vscode 的源码中,有许多服务(Service),这些服务提供不同模块的 API 给其他模块使用,在需要依赖该服务的类构造器中用装饰器的形式声明参数,开发者不需要再显示地 new 这个服务对象,在调用者被创建时,这些依赖的服务会被自动创建并传递给调用者,服务之间还能够相互依赖,这样做大幅度地降低了程序的耦合性。本文将带你简单了解 vscode 的依赖注入。

预备知识

Typescript 装饰器

如果您不太了解 typescript 装饰器,请先阅读我之前的文章《TypeScript 装饰器》

依赖注入详解

创建服务

定义一个类并在其构造函数中声明依赖的服务

class MyClass {
	constructor(
		@IMyService private readonly myService: IMyService
	) {
	}
}

我们在类中使用了一个装饰器 @IMyService,相信你看了预备知识之后,能够清楚地知道这是一个参数装饰器,该装饰器会在运行时被调用,并传入三个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 成员的名字
  • 参数在函数参数列表中的索引

那么,这个 IMyService 装饰器是如何定义的呢,让我们接下来看:

const IMyService = createDecorator<IMyService>('myService');
interface IMyService {
	_serviceBrand: undefined;
	myMethod(): any;
}

当然,这个服务接口需要具体的实现:

class MyService implements IMyService {
	_serviceBrand: undefined;
	myMethod() {
		return true
	}
}

我们发现,在声明这个接口之前,有个 createDecorator 函数定义了一个服务的装饰器,用于在构造函数中声明依赖关系以方便注入依赖。createDecorator 的主要作用是返回一个装饰器,让我们来看看是怎么实现的:

function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
	// 已经保存过的服务直接返回其装饰器
	if (_util.serviceIds.has(serviceId)) {
		return _util.serviceIds.get(serviceId)!;
	}
	
	// 声明装饰器,只能被用于参数装饰器
	const id = <any>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, false);
	};

	id.toString = () => serviceId;
	
	// 在 serviceIds 保存依赖
	_util.serviceIds.set(serviceId, id);
	return id;
}

storeServiceDependency 函数:

function storeServiceDependency(id: Function, target: Function, index: number, optional: boolean): void {
	if ((target as any)[_util.DI_TARGET] === target) {
		(target as any)[_util.DI_DEPENDENCIES].push({ id, index, optional });
	} else {
		(target as any)[_util.DI_DEPENDENCIES] = [{ id, index, optional }];
		(target as any)[_util.DI_TARGET] = target;
	}
}

storeServiceDependency 函数将传入的服务 ID (唯一的字符串 myService )及索引保存在所装饰类的一个成员 $di$dependencies 数组中, 对于例子中的 MyClass,其构造函数中的一个参数装饰器会在编译的时候被执行,把依赖记入到 $di$dependencies 数组。

MyClass['$di$dependencies'] = [
	{ id: 'myService', index: 0, optional: false }
];

服务集容器

上节讲到创建了一个服务,那么有多个服务时,就需要一个服务集,用于保存一组服务。

class ServiceCollection {
	// 服务集的 map
	private _entries = new Map<ServiceIdentifier<any>, any>();

	constructor(...entries: [ServiceIdentifier<any>, any][]) {
		for (let [id, service] of entries) {
			this.set(id, service);
		}
	}
	
	// 添加服务
	set<T>(id: ServiceIdentifier<T>, instanceOrDescriptor: T | SyncDescriptor<T>): T | SyncDescriptor<T> {
		const result = this._entries.get(id);
		this._entries.set(id, instanceOrDescriptor);
		return result;
	}

	// 是否含有该服务
	has(id: ServiceIdentifier<any>): boolean {
		return this._entries.has(id);
	}
	
	// 获取服务
	get<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> {
		return this._entries.get(id);
	}
}

创建一个服务集容器:

const services = new ServiceCollection();
// 第一个参数为服务的构造器,即 id,如使用装饰器中说到的 myService
// 第二个参数为服务的实例对象
services.set(ILoggerService, new LoggerService(logService, fileService));
services.set(ILifecycleMainService, new SyncDescriptor(LifecycleMainService));

在 vscode 中,有些服务是不依赖其他服务,仅被其他服务所依赖,如日志服务(ILoggerService),此类服务创建的时候可直接 new 服务的对象。有些服务还会依赖别的服务,需要 SyncDescriptor 封装之后保存在服务集中。

class SyncDescriptor<T> {

	readonly ctor: any;
	readonly staticArguments: any[];
	readonly supportsDelayedInstantiation: boolean;

	constructor(ctor: new (...args: any[]) => T, staticArguments: any[] = [], supportsDelayedInstantiation: boolean = false) {
		this.ctor = ctor;	//服务的构造器
		this.staticArguments = staticArguments;	// 静态参数
		this.supportsDelayedInstantiation = supportsDelayedInstantiation;	// 是否支持延迟实例化
	}
}

SyncDescriptor 是一个用于包装需要被容器实例化容器的描述符对象,它保存了对象的构造器和静态参数

实例化容器

上文讲到,我们创建了一个服务集容器,并把服务注入到了容器之中,那么就需要一个类来操作这个容器, InstantiationService 这个类就是起到这个作用。

const IInstantiationService = createDecorator<IInstantiationService>('instantiationService');

从源码中,我们也能看到,IInstantiationService 也是一个服务,他最早在 main.ts 中的 startup 函数中被初始化。

const instantiationService = new InstantiationService(services, true);

在创建了 instantiationService 实例之后,可调用 invokeFunction 方法来手动地获取服务实例,容器会自动去分析它所依赖的服务并自动实例化后返回。在寻找容器中的服务时,当在当前容器中找不到这个服务时,会去查找父容器中是否有这个服务。

_getServiceInstanceOrDescriptor<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> {
	// 在容器中根据 id 获取服务实例或描述符
	let instanceOrDesc = this._services.get(id);
	if (!instanceOrDesc && this._parent) {
		// 查找父容器的服务
		return this._parent._getServiceInstanceOrDescriptor(id);
	} else {
		return instanceOrDesc;
	}
}

如果找到的服务是个 SyncDescriptor,即这个服务还依赖其他服务,则会通过图的关系来处理服务间的依赖关系,这里是整个依赖注入的核心,放到下面详细说明。

依赖分析

在上文中讲到,在不依赖其他服务的情况下,是很简单的。当服务之间有依赖关系,随着需求的增加就会变得十分复杂。而依赖注入的核心就是这些关系的处理,以及查看存在的依赖循环,那么 vscode 是如何处理这些依赖关系的呢,让我们看下面这段代码,可能比较复杂,我们慢慢分析。

_createAndCacheServiceInstance<T>(id: ServiceIdentifier<T>, desc: SyncDescriptor<T>, _trace: Trace): T {

	type Triple = { id: ServiceIdentifier<any>, desc: SyncDescriptor<any>, _trace: Trace; };
	const graph = new Graph<Triple>(data => data.id.toString());

	let cycleCount = 0;
  // 将当前依赖推入栈
	const stack = [{ id, desc, _trace }];
  // 根据依赖关系构造有向图
	while (stack.length) {
		const item = stack.pop()!;
    // 查找或者插入节点 graph: myService, (incoming)[], (outgoing)[]
		graph.lookupOrInsertNode(item);

		// a weak but working heuristic for cycle checks
		if (cycleCount++ > 1000) {
			throw new CyclicDependencyError(graph);
		}

		// 检查所有依赖项是否存在以及是否需要首先创建它们, dependency 是我们服务所依赖的其他服务
		for (let dependency of _util.getServiceDependencies(item.desc.ctor)) {

			let instanceOrDesc = this._getServiceInstanceOrDescriptor(dependency.id);
			if (!instanceOrDesc && !dependency.optional) {
				console.warn(`[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`);
			}

			if (instanceOrDesc instanceof SyncDescriptor) {
				const d = { id: dependency.id, desc: instanceOrDesc, _trace: item._trace.branch(dependency.id, true) };
				graph.insertEdge(item, d);
				stack.push(d);
			}
		}
	}

	while (true) {
		const roots = graph.roots();
		
		// 所有 outgoing 为空但是还有节点时,认为是有依赖循环,报错
		if (roots.length === 0) {
			if (!graph.isEmpty()) {
				throw new CyclicDependencyError(graph);
			}
			break;
		}
		for (const { data } of roots) {
			// Repeat the check for this still being a service sync descriptor. That's because
			// instantiating a dependency might have side-effect and recursively trigger instantiation
			// so that some dependencies are now fullfilled already.
			const instanceOrDesc = this._getServiceInstanceOrDescriptor(data.id);
			if (instanceOrDesc instanceof SyncDescriptor) {
				// 创建实例
				const instance = this._createServiceInstanceWithOwner(data.id, data.desc.ctor, data.desc.staticArguments, data.desc.supportsDelayedInstantiation, data._trace);
				this._setServiceInstance(data.id, instance);
			}
			graph.removeNode(data);
		}
	}
	return <T>this._getServiceInstanceOrDescriptor(id);
}

vscode 是通过图来保存依赖间的关系,我们的依赖关系可以看做是一个有向图,不同的依赖可以看做是图中的一个节点,而节点间实例化的先后顺序可以看做图的边,在 vscode 中,是采用邻接表这种数据结构来表示图,即Graph 中用一个 _nodes 来保存每个顶点,且每个顶点的所有相邻(出度及入度)顶点保存在顶点对应的一张链表中。

insertEdge(from: T, to: T): void {
	const fromNode = this.lookupOrInsertNode(from);
	const toNode = this.lookupOrInsertNode(to);
	
	fromNode.outgoing.set(this._hashFn(to), toNode);
	toNode.incoming.set(this._hashFn(from), fromNode);
}

有向图构造完之后,从图中拿出所有出度构成的依赖数组,因为依赖关系是逐层往上的,即将 A 服务所依赖的其他服务依次实例化,最后再实例化 A ,一直到全部实例化完成为止。


while (true) {
  	// 出度,即本服务依赖的服务数组
		const roots = graph.roots();
		
		// 根节点为空但是图中不为空时,报错
		if (roots.length === 0) {
      // 如果所有依赖都已经实例化完成而图中还有节点则认为包含循环依赖
			if (!graph.isEmpty()) {
				throw new CyclicDependencyError(graph);
			}
			break;
		}
    // 遍历数组并以此实例化
		for (const { data } of roots) {
			// Repeat the check for this still being a service sync descriptor. That's because
			// instantiating a dependency might have side-effect and recursively trigger instantiation
			// so that some dependencies are now fullfilled already.
			const instanceOrDesc = this._getServiceInstanceOrDescriptor(data.id);
			if (instanceOrDesc instanceof SyncDescriptor) {
				// 创建实例
				const instance = this._createServiceInstanceWithOwner(data.id, data.desc.ctor, data.desc.staticArguments, data.desc.supportsDelayedInstantiation, data._trace);
				this._setServiceInstance(data.id, instance);
			}
			graph.removeNode(data);
		}
	}
	return <T>this._getServiceInstanceOrDescriptor(id);

当所有实例化全部完成,调用栈回到 _createInstance 方法,serviceArags 拿到了所有的依赖后调用 create 方法传入构造器以及其参数创建实例。

private _createInstance<T>(ctor: any, args: any[] = [], _trace: Trace): T {

		// 由服务装饰器定义的参数,根据 index 排序
		let serviceDependencies = _util.getServiceDependencies(ctor).sort((a, b) => a.index - b.index);
		let serviceArgs: any[] = [];
		for (const dependency of serviceDependencies) {
			// 获取或者创建实例
			let service = this._getOrCreateServiceInstance(dependency.id, _trace);
			if (!service && this._strict && !dependency.optional) {
				throw new Error(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id}.`);
			}
			serviceArgs.push(service);
		}

		let firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : args.length;

		// check for argument mismatches, adjust static args if needed
		if (args.length !== firstServiceArgPos) {
			console.warn(`[createInstance] First service dependency of ${ctor.name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`);

			let delta = firstServiceArgPos - args.length;
			if (delta > 0) {
				args = args.concat(new Array(delta));
			} else {
				args = args.slice(0, firstServiceArgPos);
			}
		}

		// 创建服务实例
		return <T>new ctor(...[...args, ...serviceArgs]);
	}

总结

本文从 vscode 源码的角度分析了其依赖注入的原理,简而言之,依赖分析的过程是递归向下获取所有的依赖项,再递归向上优先实例化最底层的依赖,在实例化依赖的时候,会逐级去查找依赖并实例化。用服务集容器来管理依赖,可以让每个服务集容器具有不同的能力,例如在 vscode 中,主进程和渲染进程并不共享一套服务,因为主进程和渲染进程的能力和职责不同,分开管理比较合理。

Tags