• 技术文章 >开发工具 >VSCode

    详细分析一下VSCode中的依赖注入

    青灯夜游青灯夜游2022-11-24 21:25:41转载159

    php入门到就业线上直播课:进入学习

    在阅读 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 的构造函数,导致代码耦合。

    二是在复杂项目中,我们在实例化模块 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);

    这种将依赖对象通过外部注入,避免在模块内部实例化依赖的方式,称为依赖注入 (Dependencies Inject, 简称 DI)。这在软件工程中是一种常见的设计模式,我们在 Java 的 Spring,JS 的 Angular,Node 的 NestJS 等框架中都可以看到这种设计模式的应用。

    当然,在实际应用中,由于模块众多,依赖复杂,我们很难像上面的例子一样,规划出来每个模块的实例化时机,从而编写模块实例化顺序。并且,许多模块可能并不需要第一时间被创建,需要按需实例化,因此,粗暴的统一实例化是不可取的。

    因此我们需要一个统一的框架来分析并管理所有模块的实例化过程,这就是依赖注入框架的作用。

    借助于 TypeScript 的装饰器能力,VSCode 实现了一个极为轻量化的依赖注入框架。我们可以先来简单实现一下,解开这个巧妙设计的神秘面纱。

    最简依赖注入框架设计

    实现一个依赖注入框架只需要两步,一个是将模块声明并注册到框架中进行管理,另一个是在模块构造函数中,声明所需要依赖的模块有哪些。

    我们先来看模块的注册过程,这需要 TypeScript 的类装饰器能力。我们在注入时,只需要判断模块是否已经注册,如果没有注册,将模块的 id(这里简化为模块 Class 名称)与类型传入即可完成单个模块的注册。

    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);
      };
    }

    最后只需要保证框架本身在项目运行前完成实例化即可。(在例子中表示为 injector)

    export class ServiceCollection {
      readonly providers = new Map<string, any>();
      readonly dependencies = new Map<string, any>();
    }
    
    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()

    // 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
      ) {}
    }

    其实这里的修饰符并不是真正指向类名,而是一个同名的资源描述符 id(VSCode 中称之为 ServiceIdentifier),通常使用字符串或 Symbol 标识。

    通过 ServiceIdentifier 作为 id,而不是简单粗暴地通过类名称作为 id 注册 Service,有利于处理项目中一个 interface 可能存在多态实现,需要同时多个同名类实例的问题。

    此外,在构造 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基础教程

    以上就是详细分析一下VSCode中的依赖注入的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:掘金社区,如有侵犯,请联系admin@php.cn删除

    前端(VUE)零基础到就业课程:点击学习

    清晰的学习路线+老师随时辅导答疑

    自己动手写 PHP MVC 框架:点击学习

    快速了解MVC架构、了解框架底层运行原理

    上一篇:实战:vscode中开发一个支持vue文件跳转到定义的插件 下一篇:自己动手写 PHP MVC 框架(40节精讲/巨细/新人进阶必看)

    相关文章推荐

    • ❤️‍🔥共22门课程,总价3725元,会员免费学• ❤️‍🔥接口自动化测试不想写代码?• 值的了解的一些vscode光标操作,让开发如丝般顺滑!• 聊聊ESLint与Prettier在vscode中怎么进行代码自动格式化• vscode如何连接远程服务器?(图文教程)• vscode插件分享:5个工作插件+5个摸鱼插件
    1/1

    PHP中文网