In the process of reading the VSCode code, we will find that there are a large number of decorators used in each module to decorate the module and the modules it depends on. variable. What is the purpose of doing this? In this article we will analyze it in detail. [Recommended learning: vscode tutorial, Programming video]
If there is such a module A, its implementation depends on another Module B has the capability, so how should it be designed? Very simply, we can instantiate module B in the constructor of module A, so that we can use the capabilities of module B inside module A.
class A { constructor() { this.b = new B(); } } class B {} const a = new A();
But there are two problems with this. First, during the instantiation process of module A, module B needs to be instantiated manually, and if the dependencies of module B change, the structure of module A also needs to be modified. Functions, leading to code coupling.
Second, in complex projects, when we instantiate module A, it is difficult to determine whether module B is dependent on other modules and has already been instantiated, so module B may be instantiated multiple times. If module B is heavier or needs to be designed as a singleton, this will cause performance problems.
Therefore, a better way is to hand over the instantiation of all modules to the outer framework, and let the framework uniformly manage the instantiation process of the modules, so that the above two problems can be solved.
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);
This method of injecting dependent objects from the outside to avoid instantiating dependencies inside the module is called Dependencies Inject (DI). This is a common design pattern in software engineering. We can see the application of this design pattern in frameworks such as Java's Spring, JS's Angular, and Node's NestJS.
Of course, in actual applications, due to the large number of modules and complex dependencies, it is difficult for us to plan the instantiation timing of each module and write the module instantiation sequence like the above example. Moreover, many modules may not need to be created at the first time and need to be instantiated on demand. Therefore, rough unified instantiation is not advisable.
So we need a unified framework to analyze and manage the instantiation process of all modules. This is the role of the dependency injection framework.
With the help of TypeScript’s decorator capabilities, VSCode implements an extremely lightweight dependency injection framework. We can first implement it briefly to unravel the mystery of this clever design.
It only takes two steps to implement a dependency injection framework. One is to declare and register the module into the framework for management, and the other is in the module constructor. Declare which modules you need to depend on.
Let’s first look at the module registration process, which requires TypeScript’s class decorator capability. When injecting, we only need to determine whether the module has been registered. If not, passing in the module's id (here simplified to the module Class name) and type can complete the registration of a single module.
export function Injectable(): ClassDecorator { return (Target: Class): any => { if (!collection.providers.has(Target.name)) { collection.providers.set(Target.name, target); } return target; }; }
After that, let’s take a look at how the module declares dependencies, which requires TypeScript’s property decorator capability. When injecting, we first determine whether the dependent module has been instantiated. If not, the dependent module will be instantiated and stored in the framework for management. Finally returns the module instance that has been instantiated.
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); }; }
Finally, you only need to ensure that the framework itself is instantiated before the project is run. (Represented as injector in the example)
export class ServiceCollection { readonly providers = new Map<string, any>(); readonly dependencies = new Map<string, any>(); } const collection = new ServiceCollection(); export default collection;
In this way, a most simplified dependency injection framework is completed. Since the module types and instances are saved, it enables on-demand instantiation of modules without the need to initialize all modules when the project is started.
We can try to call it, taking the example given above:
@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();
There is no need to know the instantiation timing of modules A and B, just initialize any module directly, and the framework will automatically help you Find and instantiate all dependent modules.
The above introduces the simplest implementation of a dependency injection framework. But when we actually read the source code of VSCode, we found that the dependency injection framework in VSCode does not seem to be consumed in this way.
For example, in the following authentication service, we found that the class does not have @injectable()
as a dependency collection for the class, and the dependent service also directly uses its class name as a modifier, and Not @inject()
.
// src\vs\workbench\services\authentication\browser\authenticationService.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 ) {} }
In fact, the modifier here does not actually point to the class name, but a resource descriptor id with the same name (called ServiceIdentifier
in VSCode), which is usually identified by a string or Symbol .
Use ServiceIdentifier
as the id instead of simply using the class name as the id to register the Service, which is helpful for handling the possibility of polymorphic implementation of an interface in the project, which requires multiple instances of the class with the same name at the same time The problem.
此外,在构造 ServiceIdentifier
时,我们便可以将该类声明注入框架,而无需@injectable()
显示调用了。
那么,这样一个 ServiceIdentifier
该如何构造呢?
// src\vs\platform\instantiation\common\instantiation.ts /** * The *only* valid way to create a {{ServiceIdentifier}}. */ export 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); }; 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<IActivityService>("IActivityServicePlanA"); export const IActivityServicePlanB = createDecorator<IActivityService>("IActivityServicePlanB"); export interface IActivityService {...} export class AuthenticationService { constructor( @IActivityServicePlanA private readonly activityService: IActivityService, ) {} }
模块之间的依赖关系是有可能存在循环依赖的,比如 A 依赖 B,B 依赖 A。这种情况下进行两个模块的实例化会造成死循环,因此我们需要在框架中加入循环依赖检测机制来进行规避。
本质上,一个健康的模块依赖关系就是一个有向无环图(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); } }
该方法通过获取图节点的出度,将该类的全部依赖提取出来作为roots,然后逐个实例化,并从途中剥离该依赖节点。由于依赖树的构建是逐层依赖的,因此按顺序实例化即可。当发现该类的所有依赖都被实例化后,图中仍存在节点,则认为存在循环依赖,抛出异常。
本篇文章简要介绍并实现了一个依赖注入框架,并解析了VSCode在实际问题上做出的一些改进。
实际上 VSCode 的依赖注入能力还有很多细节需要处理。例如异步实例化能力支持,通过封装 Deferred 类取得Promise执行状态,等等,在此就不一一展开了。感兴趣的同学可以参考 VSCode 源码:src/vs/platform/instantiation/common/instantiationService.ts,做更进一步的学习。
最简 DI 框架完整 demo:github.com/realDuang/d…
更多关于VSCode的相关知识,请访问:vscode基础教程!
The above is the detailed content of Detailed analysis of dependency injection in VSCode. For more information, please follow other related articles on the PHP Chinese website!