DI 引擎:VS Code 如何将 190+ 个服务连接在一起
前置知识
- ›第一篇:架构与分层
- ›第二篇:启动流程与进程架构
- ›TypeScript 装饰器与泛型
- ›依赖注入基本概念
DI 引擎:VS Code 如何将 190+ 个服务连接在一起
在第二篇文章中,我们看到 CodeMain 创建了约 15 个服务,CodeApplication 将其扩展至数十个,最终渲染侧的 Workbench 又引入了数百个已注册的单例。这一切都没有借助任何第三方 DI 框架——VS Code 拥有自己的依赖注入系统,简洁、强大,并与 TypeScript 的类型系统深度融合。理解它,是读懂整个代码库的前提。
createDecorator 与 ServiceIdentifier
VS Code 中的每个服务都通过一个 ServiceIdentifier<T> 来标识——它既是一个普通对象,也充当 TypeScript 的参数装饰器。工厂函数 createDecorator 负责创建它们:
export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
if (_util.serviceIds.has(serviceId)) {
return _util.serviceIds.get(serviceId)!;
}
const id = function (target: Function, key: string, index: number) {
if (arguments.length !== 3) {
throw new Error('@IServiceName-decorator can only be used to decorate a parameter');
}
storeServiceDependency(id, target, index);
} as ServiceIdentifier<T>;
id.toString = () => serviceId;
_util.serviceIds.set(serviceId, id);
return id;
}
这里有个巧妙的设计:id 本身是一个函数,TypeScript 会将其作为参数装饰器调用,同时它通过 ServiceIdentifier<T> 接口携带了类型标记。这种双重特性意味着,只需一行声明,例如 export const IFileService = createDecorator<IFileService>('fileService'),你就同时获得了:
- 一个类型安全的 DI 容器查找键。
- 一个用于构造函数注入的参数装饰器。
classDiagram
class ServiceIdentifier~T~ {
+type: T
+(target, key, index): void
+toString(): string
}
class createDecorator {
+createDecorator~T~(serviceId: string): ServiceIdentifier~T~
}
class _util {
+serviceIds: Map~string, ServiceIdentifier~
+DI_TARGET: string
+DI_DEPENDENCIES: string
+getServiceDependencies(ctor): Dependency[]
}
createDecorator ..> ServiceIdentifier : creates
ServiceIdentifier ..> _util : stores dependency metadata
当你写出 constructor(@ILogService private logService: ILogService) 这样的构造函数时,@ILogService 装饰器会在类定义阶段触发,将"该构造函数在指定 index 位置的参数依赖于 ILogService"这一信息记录下来。这份元数据通过 storeServiceDependency() 以 $di$dependencies 属性的形式存储在构造函数本身上。
InstantiationService:基于依赖图的解析
InstantiationService 是 DI 容器的核心。它持有一个 ServiceCollection(本质上是 Map<ServiceIdentifier, instance | SyncDescriptor>),并通过构建依赖图来解析依赖关系:
flowchart TD
A["createInstance(MyClass)"] --> B["Read @decorator metadata<br/>from MyClass constructor"]
B --> C["For each dependency,<br/>check ServiceCollection"]
C --> D{Instance exists?}
D -->|Yes| E["Use existing instance"]
D -->|No, has SyncDescriptor| F["Add to dependency graph"]
F --> G["Recursively resolve<br/>descriptor's dependencies"]
G --> H["Topological sort the graph"]
H --> I{Cycle detected?}
I -->|Yes| J["Throw CyclicDependencyError"]
I -->|No| K["Instantiate in dependency order"]
K --> L["Return MyClass instance"]
依赖图的构建和循环检测发生在内部方法 _createAndCacheServiceInstance 中。当某个服务依赖于另一个以 SyncDescriptor 形式注册的服务(即"尚未实例化")时,解析器会在图中添加一条边。收集完所有依赖后,执行拓扑排序,并按照从叶节点开始的顺序依次完成实例化。
instantiationService.ts#L21-L26 中的 CyclicDependencyError 提供了实用的诊断信息——它会调用 findCycleSlow() 来定位精确的循环路径,在开发阶段极为有用。
此外,容器还支持通过 createChild() 创建子作用域。子容器继承父容器的所有服务,同时可以覆盖其中特定的服务。Workbench 正是利用这一机制为特定功能创建独立的作用域容器。
通过 SyncDescriptor 和 Proxy 实现懒加载
SyncDescriptor 是一个简单的包装类,持有构造函数引用、静态参数,以及一个关键的 supportsDelayedInstantiation 标志。
export class SyncDescriptor<T> {
readonly ctor: any;
readonly staticArguments: unknown[];
readonly supportsDelayedInstantiation: boolean;
constructor(ctor: new (...args: any[]) => T,
staticArguments: unknown[] = [],
supportsDelayedInstantiation: boolean = false) { ... }
}
当 supportsDelayedInstantiation 为 true 时,InstantiationService 不会立即创建真正的服务实例,而是创建一个 JavaScript Proxy 作为占位符。只有当 Proxy 上的某个方法被首次调用时,才会触发真正的实例化。
sequenceDiagram
participant Consumer
participant Proxy as JS Proxy
participant DI as InstantiationService
participant Service as Real Service
Consumer->>Proxy: accessor.get(IFooService)
Note over Proxy: Proxy returned, not real instance
Consumer->>Proxy: fooService.doSomething()
Proxy->>DI: First access! Instantiate now.
DI->>Service: new FooService(deps...)
Proxy->>Service: Forward doSomething()
Service-->>Consumer: Result
Note over Proxy: Subsequent calls go<br/>directly to Service
这是一项显著的启动性能优化。许多服务在启动时就已注册,但只有在用户触发特定操作后才真正需要。如果没有懒加载 Proxy,Workbench 就必须在启动时同步实例化数百个服务,严重阻塞首次渲染。
提示: 注册新服务时,建议默认使用
InstantiationType.Delayed(对应supportsDelayedInstantiation: true),除非该服务有必须在启动时执行的副作用。
registerSingleton:全局服务注册表
registerSingleton 函数是第一篇文章中提到的 barrel 文件与 DI 容器之间的桥梁:
const _registry: [ServiceIdentifier<any>, SyncDescriptor<any>][] = [];
export function registerSingleton<T>(id: ServiceIdentifier<T>,
ctorOrDescriptor: ...,
supportsDelayedInstantiation?: InstantiationType): void {
if (!(ctorOrDescriptor instanceof SyncDescriptor)) {
ctorOrDescriptor = new SyncDescriptor(ctorOrDescriptor, [], Boolean(supportsDelayedInstantiation));
}
_registry.push([id, ctorOrDescriptor]);
}
逻辑非常直接:将 [ServiceIdentifier, SyncDescriptor] 元组推入一个模块级数组。在 Workbench 启动阶段,Workbench.initServices() 会调用 getSingletonServiceDescriptors() 将这个数组中的内容全部转移到 ServiceCollection 中:
const contributedServices = getSingletonServiceDescriptors();
for (const [id, descriptor] of contributedServices) {
serviceCollection.set(id, descriptor);
}
当你导入 workbench.desktop.main.ts 这样的 barrel 文件时,它所引入的每个模块都有机会在模块求值阶段调用 registerSingleton()。等到 initServices() 执行时,完整的服务依赖图已经声明完毕。InstantiationType 枚举控制服务是 Eager(在此循环中立即实例化)还是 Delayed(通过 Proxy 懒加载实例化)。
Disposable 模式与生命周期管理
IDisposable 接口只有一个 dispose() 方法,却是整个代码库中资源管理的基石。
src/vs/base/common/lifecycle.ts 定义了核心构建块:
classDiagram
class IDisposable {
<<interface>>
+dispose(): void
}
class Disposable {
#_register(disposable): T
+dispose(): void
}
class DisposableStore {
+add(disposable): T
+clear(): void
+dispose(): void
}
class DisposableMap {
+set(key, disposable): void
+deleteAndDispose(key): void
+dispose(): void
}
class GCBasedDisposableTracker {
-_registry: FinalizationRegistry
+trackDisposable(d): void
+markAsDisposed(d): void
}
IDisposable <|.. Disposable
IDisposable <|.. DisposableStore
Disposable *-- DisposableStore : uses internally
IDisposable <|.. DisposableMap
Disposable 基类提供了 _register() 方法,用于将子 disposable 添加到内部的 DisposableStore。当父对象被 dispose 时,所有已注册的子对象也会随之被 dispose,从而形成一棵自然的所有权树。
GCBasedDisposableTracker 的设计尤为巧妙:它利用 FinalizationRegistry API 来检测那些在没有调用 dispose() 的情况下就被垃圾回收的 disposable——这是资源泄漏的强烈信号。在开发阶段,它会输出类似 [LEAKED DISPOSABLE] CREATED via: ... 的警告,并附带创建时的调用栈。
Event/Emitter 系统
VS Code 的自定义 Event<T> 类型是一个接受监听器并返回 IDisposable 的函数——即订阅本身。Emitter<T> 类为其提供底层支持,通过 fire() 方法触发事件。
这套系统的独特之处在于 Event 命名空间提供的一组可组合算子:
Event.map(event, fn)— 转换事件载荷Event.filter(event, predicate)— 仅对满足条件的事件触发Event.debounce(event, merge, delay)— 合并高频触发的事件Event.buffer(event)— 在监听器挂载前缓冲事件Event.once(event)— 首次触发后自动取消订阅
所有算子都返回新的 Event 实例,并与 disposable 模式深度集成。这是 VS Code 响应式机制的核心骨架——各个服务暴露 onDidChange* 事件,消费者将其组合成派生信号,无需手动管理原始监听器的生命周期。
Registry 与 Contribution 模式
Registry 是一个以字符串为键的简单 Map,用于代码库内部的扩展点管理。通过 Registry.add(id, data) 注册 contribution registry,再通过 Registry.as<T>(id) 取回。
这一模式最高层次的应用是 registerWorkbenchContribution2,它将 workbench contribution 与特定的生命周期阶段绑定注册:
export const enum WorkbenchPhase {
BlockStartup = LifecyclePhase.Starting, // Blocks editor from showing
BlockRestore = LifecyclePhase.Ready, // Blocks UI state restore
AfterRestored = LifecyclePhase.Restored, // Views and editors restored
Eventually = LifecyclePhase.Eventually // 2-5 seconds after restore
}
src/vs/workbench/common/contributions.ts#L31-L62
flowchart LR
A["BlockStartup"] --> B["BlockRestore"]
B --> C["AfterRestored"]
C --> D["Eventually"]
A -.->|"Essential init<br/>(blocks first paint)"| A1["Keybindings,<br/>Theming"]
C -.->|"Non-critical<br/>(after UI visible)"| C1["Terminal restore,<br/>Extension recommendations"]
D -.->|"Background<br/>(2-5s delay)"| D1["Telemetry,<br/>Update checker"]
大多数 contribution 应使用 WorkbenchPhase.AfterRestored 或 Eventually。BlockStartup 会延迟首次渲染,应仅保留给那些必须在用户看到任何内容之前执行的 contribution。Contribution 系统会追踪每个 contribution 的创建耗时,并对超过 2ms 的情况输出警告日志。
提示:
{ lazy: true }实例化选项可将 contribution 的初始化推迟到通过getWorkbenchContribution()显式请求时才执行,非常适合那些在某些会话中可能完全不会被激活的功能。
下一篇
DI 系统、disposable、事件和 contribution 构成了 VS Code 一切功能的基础模式。下一篇文章将带我们走进 Extension Host——这是一个独立进程,第三方代码在其中运行,并通过一套拥有 140 余个代理接口的 RPC 协议与 Workbench 保持通信。