第 87 期 - VSCode 依赖注入与组件实现解析
摘要
文章先介绍了 VSCode 的依赖注入架构,包括概念、相关操作等,然后阐述其组件实现,如组件通信、渲染方式等,还对比了 VSCode 原生开发模式与 React/Vue 开发模式的差异及原因
1. 依赖注入架构
1.1 依赖注入的背景与概念
- 在 VSCode 中存在众多服务类,服务间可能相互调用,如 EditorService 调用 FileService。当服务类增多时,手动管理依赖和实例化顺序会带来很重的心智负担。例如存在 A 依赖 B,B 依赖 C 等情况时,需要按顺序实例化并传递依赖。
- 为解决对象间耦合度过高的问题,软件专家提出 IOC 理论,其中依赖注入(DI)是常见方式。采用 DI 后,如 ServiceA 不需要直接实例化 ServiceB,而是通过容器控制程序将外部实例化的 ServiceB 注入到 ServiceA 中。相关代码如下:
class ServiceA {
constructor(@IServiceB private _serviceB: ServiceB) {}
}
class ServiceB {
constructor(@IServiceC serviceC: ServiceC) {}
}
1.2 VSCode 中的概念
- Contribution:一般是业务模块,是最上层业务模块,通常不会被其他模块依赖,一个 Contribution 对应一个模块,内部包含 UI、Model 等模块,如编辑器里的查找替换功能就是一个 Contribution。
- Registry:是业务模块的集合,随着项目复杂,Contribution 增多,如左侧菜单的各个模块都是 Contribution,Registry 将这些 Contribution 归类。
- Service:是基础服务,提供基础能力可被多个 Contribution 共享。创建 Service 时,需要先实现接口,创建 service id,再创建 Service。例如:
// 先实现一个接口
interface ITestService {
readonly _serviceBrand: undefined;
test: () => void;
}
// 再创建一个 service id
const ITestService = createDecorator<ITestService>('test-service');
// 再创建 Service
class TestService implements ITestService {
public readonly _serviceBrand: undefined;
test() {
//...
}
}
1.2.1 接口的作用
- 接口用于实现 Service 之间不互相依赖具体实现,做到面向接口编程。以账号服务为例,不同登录方式实现不同,但依赖登录信息的组件应只依赖接口,在 VSCode 的 Electron 和 Web 环境注册的 Service 实现可能不同但接口相同。
1.2.2 createDecorator 的功能
- createDecorator 主要创建装饰器,该装饰器调用 setServiceDependency 将 serviceId 设置到被装饰类的 DI_DEPENDENCIES 属性上,从而建立类之间的关联关系。例如 Test2Service 依赖 TestService 时,通过
@ITestService
建立关联。
1.2.3 InstantiationService 相关
- Service 有两种访问方式,一种是通过 DI 在构造函数引入,另一种是通过 instantiationService.invokeFunction 形式拿到 accessors 进行访问。
- 在实例化 Service 时,先创建 ServiceCollection 关联 ITestService 和 TestService,再实例化容器 Service(InstantiationService),然后可以通过 invokeFunction 的 accessors.get 获取实例。
- Service 只有在被访问时才实例化,invokeFunction 中如果存在异步需要特殊处理。_getOrCreateServiceInstance 会根据 serviceId 查找 Service 类,找不到会从 parent 查找。容器服务可以嵌套,parent 通常也是一个 instantiationService。如果注册时传入 supportsDelayedInstantiation 会进行延迟初始化减轻首屏负担,否则调用 _createInstance 创建实例。
1.2.4 createInstance 相关
- 除了 Service,VSCode 还有 Manager,部分用 createInstance 实例化。用 createInstance 实例化的类有 DI 能力。创建实例时,GetLeadingNonServiceArgs 会从构造函数参数类型里剔除带 _serviceBrand 的参数,所以创建实例时可以不传依赖的 Service。
2. 组件化实现
2.1 组件结构
- VSCode 的复杂 UI 模块采用 MVC 形式组织,以 Controller 为入口创建 Model 和 View 层。例如查找替换功能的入口是 FindController,被当做 contribution 挂载到编辑器实例上,同时用户操作和快捷键分别注册为 Action 和 EditorCommand 到 EditorContributionRegistry,Controller 提供 public 方法供调用。
- 在 FindController 中会创建 FindWidget、FindReplaceState、FindModel 等实例作为 View 和 Model 层的桥梁。
2.2 Model 和 State
- FindReplaceState 负责维护查找状态和匹配结果,可理解为纯粹的 Store,State 层不是必要的。Model 层包含 State,做查找替换业务逻辑,监听 State 状态变更从 Editor 进行搜索并更新 FindReplaceState。
2.3 Widget
- 以 Toggle 组件为例,它继承 Widget 类,Widget 类是 UI 组件基类,监听 DOM 事件并分发。Toggle 支持传入 options,内部创建 DOM 节点,直接操作 DOM 更新 UI,将 get/set 方法暴露出去,调用简单。FindWidget 也继承 Widget 类,初始化构建 DOM,查找输入框和替换输入框通过 Widget 创建,Widget 具有组合能力且能监听 State 状态变更更新 UI。
2.4 组件通信
- 父子组件通信:父组件持有子组件,可直接调子组件 set 方法更新子组件,子组件内部变更可抛事件通知父组件更新。
- 兄弟组件通信:需父组件或 Controller 持有两个组件,组件 A 内部变化抛事件,父组件监听到后调用组件 B 的 set 方法更新。如查找替换组件中,搜索值修改会引起右侧匹配结果更新,涉及多个组件间的事件传递和状态更新。
2.5 与 React/Vue 开发模式对比
- 在 React/Vue 出现前,原生 JS、jQuery 开发模式不适合大型项目,因为 jQuery 时期模块化和组件化概念薄弱,用 AMD/CMD 做模块化、jQuery 插件做组件化不够彻底且上手成本高,容易出现 DOM 节点到处绑事件难以调试的情况,使用模板引擎更新效率低、DOM 重绘开销大。而在 VSCode 中,每个组件只暴露 getter/setter,内部变更通过事件通知,组件间通信用事件形式,组件和模块划分清晰,通过细粒度更新 DOM 属性性能比 React/Vue 更高。
扩展阅读
Made by 捣鼓键盘的小麦 / © 2025 Front Talk 版权所有