关于web:vscodeDI-依赖注入实现原理

2次阅读

共计 9758 个字符,预计需要花费 25 分钟才能阅读完成。

前言

上次咱们解释了 vscode 这种大型项目设计依赖注入治理海量的模块有什么益处。也提到了 DI 必须有这样一种机制:

1、模块与模块之间的无源码依赖(这里的模块次要指类)

2、只依赖接口 / 形象,不依赖具体实现

3、模块的创立,循环援用、谬误等能够主动被捕捉到

那么这样的一种机制如何实现呢?咱们先展现一下 vscode 源码的经典片段

// mian.ts
// 独立的 app 初始化阶段的文件中
let collection = new ServiceCollection();
let service = new InstantiationService(collection);
collection.set(IService1, new Service1());

// IService1.ts
// 独立的接口文件

let IService1 = createDecorator<IService1>("service1");
interface IService1 {
    readonly _serviceBrand: undefined;
    c: number;
}

// Service1.ts
// Service1 是对 IService1 接口的具体实现
class Service1 implements IService1 {
    declare readonly _serviceBrand: undefined;
    c = 1;
}

// Service1Consumer.ts
// 依赖 IService1 接口的模块

class Service1Consumer {constructor(@IService1 service1: IService1) {assert.ok(service1);
        assert.equal(service1.c, 1);
    }
}

装璜器

从下面咱们能够看到 Service1Consumer 模块 对 IService1 接口有依赖,通过 @IService1[第一个] 装璜器润饰 service1,参数类型也叫 IService1[第二个]。这里的两个 IService1 是同名,但性能不一样的标识,能够设想为函数重载(实际上一个是函数,一个是接口)。为什么要把装璜器和接口名定义成一样的,这里是 vscode 的特地设计,上面再讲。

大部分前端开发都接触过 typescript,但不肯定深刻应用过,我先简略介绍下装璜器语法。ts 的装璜器和其余语言相似,比方 python 外面也有。装璜器是给类的申明及成员上通过元编程语法增加标注提供了一种形式。
在 ts 外面有类装璜器、函数装璜器、拜访器装璜器、属性装璜器、参数装璜器。vscode 的依赖注入借助的正是参数装璜器。

参数装璜器申明在一个参数申明之前(紧靠着参数申明)。参数装璜器利用于类构造函数或办法申明。

参数装璜器表达式会在运行时当作函数被调用,传入下列 3 个参数:

  1. 对于动态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 参数在函数参数列表中的索引。

留神   参数装璜器只能用来监督一个办法的参数是否被传入。

在下面 Service1Consumer 申明的时候,装璜器会先被调用,传人参数第一个 Service1Consumer 类的构造函数,第二个 undfined(这里没有成员名字), 第三个 0 参数的索引。

实质上,装璜器是一种不扭转原模块的状况下,给对象减少新的性能。vscode 利用这个个性在构造函数中进行标注,触发依赖收集。

依赖注入的具体实现

好了,让咱们开始实现一个简略依赖注入模型。次要三个过程,依赖收集,依赖贮存,最初依赖生产——即主动注入依赖的环节。

依赖贮存

为了有一个适合的中央贮存收集的依赖,咱们必须有储存器 ServiceCollection(在这个源码同名文件中),作为依赖的作用范畴,它的性能很简略,提供一个 Map 字段,增加 set、get、has 三个办法,别离示意设置,获取,是否蕴含。

class ServiceCollection {
    // ...
    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> {// ...}

    has(id: ServiceIdentifier<any>): boolean {// ...}

    get<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> {// ...}
}

如下,储存器只负责接管一对 key、value,别离是 id 标识符和一个具体的依赖 service。


let collection = new ServiceCollection();
// 把依赖初始化提前存起来
collection.set(IService1, new Service1());
let service = new InstantiationService(collection);
// 生产依赖
service.createInstance(Service1Consumer);

储存器会个别会在整个利用的要害初始化阶段将依赖增加进去。如果有多个利用上下文,能够创立多个储存器。

收集依赖

依赖在被其余模块生产的时候,它们之间的依赖关系如何创立的呢?

这里装璜器起到桥梁的作用,咱们看下具体步骤:

  1. 先定义装璜描述符
    let IService1 = createDecorator<IService1>('service1');

createDecorator 的性能是传入一个 class 的字符标记,这个字符串是对应惟一类名。

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, false);
    };
  // serviceId 作为 key 记录下来
    id.toString = () => serviceId;

    _util.serviceIds.set(serviceId, id);
  // 这个 id 返回不会被销毁,因为被 storeServiceDependency 援用了
    return id;
}

而后把这个惟一类名作为返回函数的属性通过 toString 记录起来,这个返回函数正式装璜器函数,它承受三个参数。下面的代码利用了 js 的闭包个性,id 是返回给 IService1 的装璜器函数,同时作为它本身外部调用 storeServiceDependency 的一个参数,这个函数调用结束不会被销毁,而是依然贮存在函数栈中,闭包的基础知识不多做解释。

失去装璜器办法后,它会在哪里被应用呢,哪个模块依赖于 IService1 这个接口(同名接口),那么就在这个模块的结构函数参数中用装璜器符号 @IService1 润饰参数。

  1. 申明类和调用装璜器
class Service1Consumer {constructor(@IService1 service1: IService1) {assert.ok(service1);
        assert.equal(service1.c, 1);
    }
}

咱们看到,当初 Service1Consumer 被申明,装璜器会提前被主动调用。它的 第一个参数 targe 为 Service1Consumer 本身,而它的最初一个参数是参数 service1 的索引。

咱们眼帘转移装璜器办法自身,刚刚讲到 id 被作为闭包变量依然贮存在函数中,装璜器运行的时候 storeServiceDependency 办法负责收集依赖,id 蕴含 toString 记录的依赖类名,而 target 记录了被依赖的类,index 是作为 targe 类的索引.

// 具体逻辑参考第一步,id 从闭包中获取
storeServiceDependency(id, target, index, false);

最终,咱们失去了 service1 和 Service1Consumer 的依赖关系映射。

Service1Consumer.$dependes=id['serviceId']

到这里,咱们曾经晓得模块的依赖关系是如何被 DI 发现的了,回到最开始埋的疑难,为什么装璜器和接口的应用同名字符 IService1?这是一种无意奇妙设计,对于接口的设计者来说,须要定义一个接口文件,同时定义这个接口装璜器,默认状况保持一致。

export let IService1 = createDecorator<IService1>("service1");
export interface IService1 {
    readonly _serviceBrand: undefined;
    c: number;
}

接口的定义只有一个,但有些时候,具体的实现可能有很多。也就是说,咱们定义好了接口,能够提供多份具体实现。接口文件默认返回一个同名装璜器,通常这个接口和具体实现是惟一的。对于调用方来说,它只须要定义构造函数的类型为指定接口名,用装璜器装璜参数,指定主动注入的模块,而这个模块正是 createDecorator<IService1>('service1') 所标识的内容 service1,也是通过它主动缓存了依赖关系,默认放弃装璜器与接口同名,对调用者,进一步屏蔽了这层背地实现,放弃简略通明。vscode 全副模块设计都采纳了这样一种办法。

// 小王的实现计划 1【默认】export let IService1 = createDecorator<IService1>("service1");
// 小明的另一种实现计划
export let IService2 = createDecorator<IService1>("service2");
// 小华的另一种实现计划
export let IService3 = createDecorator<IService1>("service3");
export interface IService1 {
    readonly _serviceBrand: undefined;
    c: number;
}

生产实例

当初,咱们曾经有被提前贮存好的依赖实例,也建设了模块消费者和依赖之间的关系。最初一步就是创立消费者实例。

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

        // 从依赖收集缓存中获取依赖标识
        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);
            // ..
            serviceArgs.push(service);
        }
      // 略 ...

        // 把依赖的示例和参数传进去,而后 new 一个构造函数
        return <T>new ctor(...[...args, ...serviceArgs]);
    }

示例的生产有两种形式,第一是调用 createInstance 创立,它会从缓存的依赖关系中找出依赖,再依据储存器中的 key、value 拿到实例对象,作为传给类的构造函数。另外一种上面探讨。

/
let collection = new ServiceCollection();
// 把依赖初始化提前存起来
collection.set(IService1, new Service1());
let service = new InstantiationService(collection);
// 生产依赖
service.createInstance(Service1Consumer);
class Service1Consumer {constructor(@IService1 service1: IService1) {
    // 依赖被注入
        assert.ok(service1);
        assert.equal(service1.c, 1);
    }
}

到这里,咱们曾经把一个简略的 DI 注入模型进行了实现,通过装璜器建设依赖关系,而后实例化 class 的时候依据依赖标识别从储存器中取依赖注入进去。

通过描述符创立(提早创立)

下面的模型依然比较简单,依赖被创立,并储存起来的时候,会提前耗费大量内存。实在的场景是心愿依赖在真正须要的时候才被主动创立,而非提前实例化。

let collection = new ServiceCollection();
let service = new InstantiationService(collection);
// Service1 只是一个类描述符,对象并没有被创立
collection.set(IService1, new SyncDescriptor<IService1>(Service1));
// IDependentService 也是一个类名,它依赖 Service1
collection.set(
    IDependentService,
    new SyncDescriptor<IDependentService>(DependentService)
);

service.invokeFunction((accessor) => {
    // 这里能够取到实例化的对象
    let d = accessor.get(IDependentService);
    assert.ok(d);
    assert.equal(d.name, "farboo");
});

如上代码示例,咱们的类没有被实例化,而是作为描述符增加到储存器中,这个过程并不会有额定对象创立的内存耗费。在须要的时候,咱们再通过 invokeFunction 拿到实例化的对象。

期间 IDependentService 对 IService1 有依赖,或者进一步,IService1 也可能有其余更深的依赖,咱们心愿在调用过程,这些没有被提前实例化的依赖,能过被主动创立进去,而后返回给消费者。这个过程是如何进行的呢?

依赖剖析

模块依赖实质上是一颗依赖树,咱们可依据模块 F 对其依赖及其子依赖,做一个广度优先遍历。

F 模块依赖 B、G 模块,B 模块依赖 A、D 模块以此类推,把遍历进去的模块作为一个节点贮存在依赖树之中。

const stack = [{id, desc, _trace}];
// 广度优先遍历
  while (stack.length) {
  // 从最近的依赖节点开始
  const item = stack.pop()!;
  graph.lookupOrInsertNode(item);

  // 依赖深度的下限
  if (cycleCount++ > 1000) {throw new CyclicDependencyError(graph);
  }

  // 获取子依赖
  for (let dependency of _util.getServiceDependencies(item.desc.ctor)) {let instanceOrDesc = this._getServiceInstanceOrDescriptor(dependency.id);
      const d = {
        id: dependency.id,
        desc: instanceOrDesc,
        _trace: item._trace.branch(dependency.id, true),
      };
      // 贮存依赖关系
      graph.insertEdge(item, d);
      // 持续入栈遍历
      stack.push(d);
  }
}

下面是简化版的依赖遍历源码,能够联合我写的正文浏览。

依赖创立

拿到依赖关系后,下一步就是按程序一一创立依赖,依赖创立的程序应该是怎么样的呢?天然是要从最末端的叶子节点开始。

因为咱们的依赖方向是从上到下,不能循环依赖的,能够先把依赖树转化成一个有序数组,每个数组节点蕴含 from 和 to,即父节点和子节点的意思。

遍历转换后的有序数组,拿到子节点为 null 的依赖进行优先实例化。

实例化完结后,再从有序数组中删除节点即可。如此重复下面的逻辑,最初直到节点为空,则返回函数。

function () create {while (true) {
  // 拿到叶子节点
  const roots = graph.roots();
   // 循环依赖检测
  if (roots.length === 0) {if (!graph.isEmpty()) {throw new CyclicDependencyError(graph);
    }
    break;
  }
  // 创立依赖
  for (const { data} of roots) {const instanceOrDesc = this._getServiceInstanceOrDescriptor(data.id);

      // 增加到依赖收集器中
      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);
}

循环依赖检测

循环依赖的检测实际上在两个中央有解决,第一是下面依赖剖析的过程,如果两个依赖是循环的,依赖树会进入有限深度,而依赖深度的下限 为 1000,这种状况会抛出谬误,但也可能是利用过于简单依赖层级达到 1000,谬误也会被抛出。

// 参考下面更具体的上下文
 if (cycleCount++ > 1000) {throw new CyclicDependencyError(graph);
  }

另外一处是依赖创立过程,依赖的叶子节点曾经为空,但有序数组中却还有模块的状况:


if (roots.length === 0) {if (!graph.isEmpty()) {throw new CyclicDependencyError(graph);
    }
    break;
  }

实践上,依赖分析阶段只有断定循环援用,曾经抛出谬误,此处就不可能会被执行,这里额定的检测看上去有点多余。

循环遍历依赖创建对象后,之后的逻辑和后面实例注入相似。

其余

单例

vscode 中大量模块是已单例模式创立的,在入口文件中能够看到。

/// src/vs/workbench/workbench.web.main.ts
registerSingleton(IWorkbenchExtensioManagementService, ExtensionManagementService);
registerSingleton(IAccessibilityService, AccessibilityService, true);
registerSingleton(IContextMenuService, ContextMenuService);
registerSingleton(ITunnelService, TunnelService, true);
registerSingleton(ILoggerService, FileLoggerService);
registerSingleton(IUserDataSyncLogService, UserDataSyncLogService);
registerSingleton(IUserDataSyncStoreService, UserDataSyncStoreService);

单例能够了解为全局只初始化一次,vscode 大部分模块,比方日志、标题栏等模块会搁置在此。实现形式很简略,在入口把依赖全副 import 进来,而后注册到单例数组,最初在 workbenk 模块工作台中用 getSingletonServiceDescriptors 取出全副注册进依赖储存器中。单例通过描述符注册,所以也是调用时才会被创立。

// 单例对象寄存
const _registry = [];
  export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, ctorOrDescriptor: { new(...services: Services): T } | SyncDescriptor<any>, supportsDelayedInstantiation?: boolean): void {
  // 通过描述符创立
  if (!(ctorOrDescriptor instanceof SyncDescriptor)) {ctorOrDescriptor = new SyncDescriptor<T>(ctorOrDescriptor as new (...args: any[]) => T, [], supportsDelayedInstantiation);
  }

  _registry.push([id, ctorOrDescriptor]);
  }

// 导出依赖
export function getSingletonServiceDescriptors(): [ServiceIdentifier<any>, SyncDescriptor<any>][] {return _registry;}


/// 文件 src/vs/workbench/browser/workbench.ts
// 全副的单例模块被增加进依赖储存器
const contributedServices = getSingletonServiceDescriptors();
for (let [id, descriptor] of contributedServices) {serviceCollection.set(id, descriptor);
}

vscode 除了外围模块和不能提早创立模块,外部扩大组建简直都是以单例的模式注册的。这是 源码中一个醒目的正文,揭示开发者不要在这里注册依赖。

createChild 派生实例


let service = new InstantiationService(new ServiceCollection([IService1, new SyncDescriptor(CtorCounter)]));
service.createInstance(Service1Consumer);


let child = service.createChild(new ServiceCollection([IService2, new Service2()]));
child.createInstance(Service1Consumer);

模块中如果想独立于默认依赖的作用范畴,能够通过用 createChild 去创立。它仅仅是返回一个新 InstantiationService 实例。

vscode di 模块的剖析就到这里了,次要包含:提前贮存依赖,申明装璜器建设依赖关系,最初通过广度优先遍历收集 Descriptor 子依赖,保留到树结构中,循环把对象创立进去,注入到被依赖的模块之中。

正文完
 0