简单聊聊VSCode中依赖注入的原理

本篇文章给大家浅析vscode中依赖注入的原理,聊聊依赖注入做了什么?依赖注入怎么做?希望对大家有所帮助!

简单聊聊VSCode中依赖注入的原理

团队推行 「依赖注入」有一段时间了,但每次使用时都觉得很陌生,有很多概念总是不知所云:服务id,服务描述符,服务装饰器等等。

可能是因为不懂得其中原理,使用时都有种「虚」的感觉,最近通过阅读 VS Code 源码,拜读团队大佬的分享文章,力图理清其中的原理,在这里做一个简单的核心逻辑介绍。

依赖注入做了什么

假设以下情况:

  • 服务模块 A,依赖服务 B;

  • 服务模块 B;

  • 功能模块 Feature,依赖服务 A 和 B;

按照普通的写法就是:

class B {}  class A {     constructor() {         // 在 A 的构造器中 new B         this.b = new B();     } }  class Feature {     constructor() {         this.a = new A();         this.b = new B();     } }  // 使用时 const feature = new Feature();

代码简单明了,存在一些问题,比如:如果 A 和 Feature 依赖的 B 需要是同一个实例,以上的写法将会初始化两个 B 实例。【推荐学习:vscode教程vscode教程

简单修改一下:

class A {     constructor(b: B) {         this.b = b;     } }  class Feature {     constructor(a, b) {         this.a = a;         this.b = b;     } }  // 使用时 const b = new B(); const a = new A(b); const feature = new Feature(a, b);

某个模块初始化时,先在外部将其所依赖的模块创建出来,通过参数的形式传入功能模块。这样的写法就是「依赖注入」。

现在这种写法的问题在于:手动传参的形式,必须人工保证 new 的顺序,即必须获得 a, b 实例才能执行 new Feature。

当依赖关系变得复杂时,创建一个功能模块之前很有可能需要无数个基础模块,这时候复杂度将会非常高。类似于这种感觉:

简单聊聊VSCode中依赖注入的原理

想象一种模式:存在一个模块控制器,或者说「服务管理器」来管理这些依赖关系:

class Feature {     // 声明这个模块依赖 idA, idB     idA     idB }  // 告知「服务管理器」,怎么找对应的模块 services[idA] = A; services[idB] = B;  // 使用时 const feature = services.createInstance(Feature);

这个 services 承载的不就是之前的「手工」过程吗?
在 createInstance(Feature) 时,分析 Feature 所依赖的模块:

  • 如果所依赖的模块还未创建出实例,递归创建出该服务实例,最终返回;

  • 如果所依赖的模块已有实例,返回该实例;

  • 找齐后通过参数注入 Feature,完成初始化;
    vscode 实现的正是这么一套「依赖注入体系」。

依赖注入怎么做?

要实现这样一套功能,大致需要:

  • 一个类如何声明其依赖的服务 id,即给定一个 类,外部如何知道他依赖了哪些服务?

  • 如何管理管理服务?

  • 如何创建某个模块?

下文会实现一个最简单的模型,涵盖主体流程。

添加依赖信息

如何给一个 类 打上烙印,声明它所依赖的服务呢?
将问题再次抽象:如何给一个类加上额外的信息?
其实,每个类在 es5 下都是 function,而每个 Function 说到底也只是 Object ,只要给 Object 加上几个字段来标识所需要的服务 id,就可以完成所需要的功能。
通过 「参数装饰器」的写法,可以很容易做到这一点:

// 参数装饰器  const decorator = (     target: Object, // 被装饰的目标,这里为 Feature     propertyName: string,      index: number // 参数的位置索引 ) => {     target['deps'] = [{        index,        id: 'idA',    }]; } class Feature {     name = 'feature';     a: any;     constructor(         // 参数装饰器         @decorator a: any,     ) {         this.a = a;     } } console.log('Feature.deps', Feature['deps']); // [{ id: 'idA', index: 0 }]

通过这种方式,通过 Feature (之后会称之为 构造器 ctor)就可以获取到 serviceId。

服务管理

使用 map 来进行管理,一个 id 对应一个 服务 ctor。

class A {     name = 'a'; }  // 服务集 class ServiceCollection {     // 服务集合     // key 为服务标识     // value 为 服务ctor     private entries = new Map<string>();      set(id: string, ctor: any) {         this.entries.set(id, ctor);        }      get(id: string): any {         return this.entries.get(id);     } }  const services = new ServiceCollection();  // 声明服务 A id 为 idA services.set('idA', A);</string>

示意图如下:

简单聊聊VSCode中依赖注入的原理

现在,就可以通过 Feature 来找到所依赖的服务的构造器了

// 通过 Feature 找到所依赖的 A const serviceId = Feature['deps'][0].id; // idA console.log(     'Feature.deps',      services.get(serviceId) // A );

模块创建

具体思路为:

  • 如果所依赖的模块还未创建出实例,递归创建出该服务实例,最终返回;

  • 如果所依赖的模块已有实例,返回该实例;

  • 找齐后通过参数注入 Feature,完成初始化;

这里先上一个简单的 demo,只有一层的依赖(即所依赖的服务没有依赖其他服务),简单的讲,就是没有递归能力:

class InstantiationService {     services: ServiceCollection;      constructor(services: ServiceCollection) {         this.services = services;     }      createInstance(ctor: any) {         // 1. 获取 ctor 依赖的 服务id         // 结果为: ['idA']         const depIds = ctor['deps'].map((item: any) =&gt; item.id);          // 2. 获取服务 id 对应的 服务构造器         // 结果为:[A]         const depCtors = depIds.map((id: string) =&gt; services.get(id));          // 3. 获取服务实例         // 结果为: [ A { name: 'a'} ]         const args = depCtors.map((ctor: any) =&gt; new ctor());          // 4. 依赖的服务作为参数注入,实例化所需要模块         // 结果为:[ Feature { name: 'feature', a }]         const result = new ctor(...args);          return result;     } }  const instantiation = new InstantiationService(services);  // 使用时 const feature = instantiation.createInstance(Feature);

至此,依赖注入的核心流程实现完毕,要使用 Feature 时,只需要调用 createInstance,不用管他所依赖的服务是否被初始化,instantiation 帮我们做了这个事情。

总结

本文简单实现一个 demo 级别的「依赖注入」模型,简单实现了:

  • 模块声明所需要的依赖;

  • 服务管理;

  • 模块创建;

以此为基础,可以拓展出一些高级功能:

  • 模块创建(递归):VSCode 用了 + 图 做了这件事,算法也不复杂;

  • 依赖收集:可用于分析每个模块的依赖,且可以检测是否存在「循环依赖」;

  • 模块销毁:当模块销毁时,递归销毁他所依赖的服务实例;

  • 延迟初始化:创建依赖的服务时,选择创建一个 proxy ,当真正使用时才真正创建实例;

  • 异步依赖:当依赖的服务的创建过程是异步的情况下,如何执行创建逻辑;

vscode教程 本文代码看这里。
vscode教程 参考 VSCode 整个依赖注入体系写的代码,进阶可以看这里。

参考资料

VS Code 源码 位置:src/vs/platform/instantiation/common
本文借鉴了代码思路,且命名也高度一致(手动狗头

更多关于VSCode的相关知识,请访问:vscode教程!!

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享