> 개발 도구 > VSCode > VSCode의 종속성 주입에 대한 자세한 분석

VSCode의 종속성 주입에 대한 자세한 분석

青灯夜游
풀어 주다: 2022-11-24 21:25:41
앞으로
2417명이 탐색했습니다.

VSCode의 종속성 주입에 대한 자세한 분석

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);
로그인 후 복사

모듈 내부의 종속성 인스턴스화를 피하기 위해 외부에서 종속 개체를 주입하는 이 방법을 종속성 주입(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<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의 인스턴스화 타이밍을 알 필요가 없습니다. 모듈을 직접 초기화하면 프레임워크가 자동으로 모듈 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라고 함)를 가리킵니다. . @injectable()作为类的依赖收集,并且依赖服务也直接用其类名作为修饰器,而不是@inject()

// 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(&#39;@IServiceName-decorator can only be used to decorate a parameter&#39;);
    }
    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;
  }
}
로그인 후 복사
로그인 후 복사

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

通过 ServiceIdentifier

단순히 클래스 이름을 통해 서비스를 ID로 등록하는 대신 ServiceIdentifier를 ID로 사용하면 프로젝트의 인터페이스가 다형성 구현을 가질 수 있고 여러 인스턴스가 필요할 수 있는 문제를 처리하는 데 도움이 됩니다. 동시에 같은 이름을 가진 클래스. 🎜

此外,在构造 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(&#39;@IServiceName-decorator can only be used to decorate a parameter&#39;);
    }
    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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:juejin.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
최신 이슈
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿