建筑风格和原则#
VS Code 采用分层和模块化的软件架构方法来构建其核心包。核心包旨在通过扩展 API 进行扩展,该 API 提供了 VS Code 中几乎所有可用的功能。这种架构风格有助于将代码编辑器的不同责任分开,以便于实现和测试,使开发人员清楚地知道他们应该在何处实现特定功能的部分。
每一层与其下方的层直接通信。基础层通过在基础层中定义的平台特定依赖项与平台层进行通信,而平台层通过扩展 API 与扩展层进行通信。在接下来的部分中,我们将主要关注 VS Code 的基础层、平台层和扩展层的分析。
基础层#
基础层建立在 ElectronJS 框架之上,提供可以在任何其他层中使用的一般实用程序和用户界面构建块。这些组件使用 HTML、CSS 和 JavaScript 等 Web 技术构建。基础层还提供文件系统实用程序,使 VS Code 能够与文件系统进行交互,例如读取和写入文件、导航目录以及创建新文件和目录。更重要的是,这一层包括扩展管理系统,允许用户安装、管理和更新扩展。它是使用 Web 技术和 ElectronJS API 构建的,并提供了一个直观的界面来管理扩展。
基础包包括几个子包,例如 common、electron-browser 和 node。这些子包中的每一个都包含特定于该包的模块。common 包包括跨不同平台和环境使用的模块。这包括处理文本缓冲区、处理文件和目录以及提供基本 UI 组件(如按钮和菜单)的模块。electron-browser 包包括特定于 ElectronJS 平台的模块,该平台用于在桌面上运行 VS Code。这包括处理 UI 元素、管理 Web 视图和与 ElectronJS API 交互的模块。node 包包括特定于 NodeJS 运行时的模块,该运行时用于在服务器和其他环境中运行 VS Code。这包括处理进程、文件和目录的模块,以及提供低级网络功能的模块。
总体而言,基础层通过扩展 API 与其他层进行交互,用户界面提供核心功能,其他层在此基础上构建以创建功能齐全的代码编辑器。
平台层#
VS Code 的平台层负责通过依赖注入提供所有服务,这是一种连接器类型。平台层为软件的不同模块和组件提供基本基础设施和公共服务。这种方法使模块之间的耦合更松散,并促进模块化和可扩展性。此外,平台层还提供扩展服务,允许扩展向平台注册自己并与其他服务和模块进行交互。
平台包包括几个子包,例如 services、configuration 和 workspace。services 包包括为应用程序的其余部分提供服务的模块,例如处理文本编辑功能的文本模型服务和提供对文件系统访问的文件服务。configuration 包包括处理编辑器配置的模块,例如提供有关文件语法的信息的语言配置服务和处理用户设置的设置服务。workspace 包包括处理与工作区相关功能的模块,例如提供有关工作区中文件信息的工作区服务和提供搜索文件的方法的搜索服务。代码还使用事件系统允许应用程序的不同部分相互通信。这使得向应用程序添加新功能和响应用户输入变得容易。
扩展层#
VS Code 的扩展层提供通过第三方扩展扩展软件功能的能力。这一层建立在平台层之上,允许开发人员创建自己的模块和服务,这些模块和服务可以与软件的其余部分进行交互。扩展 API 提供了一套丰富的功能和方法,用于创建和管理扩展,而语言支持、编辑器自定义和调试功能则提供了创建更个性化开发环境的额外能力。通过扩展层,开发人员可以创建新功能和服务,这些功能和服务可以与软件的其余部分无缝集成。
扩展包包括几个子包,例如 commands、languages 和 views。commands 包包括提供可以在编辑器中执行的命令的模块。languages 包包括为不同编程语言提供语言支持的模块。views 包包括为不同文件类型或功能提供视图和编辑器的模块。值得注意的是,代码使用 API 作为连接器,允许扩展与基础层和平台层进行交互,并访问它们提供的功能。当安装扩展时,它会加载到与主编辑器进程不同的单独进程中。这允许扩展在单独的上下文中运行,而不会干扰核心编辑器功能。扩展进程通过扩展 API 提供的通信通道与主编辑器进程进行通信。
基本原则#
VS Code 的架构原则围绕模块化、可扩展性和灵活性这几个概念。
-
模块化:VS Code 的架构设计为模块化,每一层由多个独立模块组成。这允许更大的灵活性和维护的便利,因为可以在不影响整体系统的情况下添加、删除或替换模块。
-
可扩展性:VS Code 设计为高度可扩展,使用 Web 技术和 VS Code 扩展 API。这使得开发人员可以通过扩展轻松添加新功能和功能到编辑器中。
-
灵活性:VS Code 的设计也很灵活,能够支持多种编程语言、开发环境和平台。它建立在 Web 技术和本机代码的组合之上,允许它在多个操作系统上运行,并且可以轻松定制以满足不同开发人员的需求。
在 VS Code 的设计中还采用了其他架构原则,包括关注点分离、模块化和分层架构。这些原则有助于确保系统易于理解、维护和扩展。
Electron#
- 使用 Web 技术编写 UI,并使用 Chrome 浏览器引擎运行
- 使用 NodeJS 操作文件系统并发起网络请求
- 使用 NodeJS C++ 插件调用操作系统的本机 API
在 Electron 中,有两种类型的进程:主进程和渲染进程:
- 主进程:主进程负责创建浏览器窗口并管理应用程序的各个方面,例如系统级事件、菜单和对话框。它在 Node.js 环境中运行,并可以访问本机操作系统 API。主进程还提供 IPC 机制,以便不同部分的应用程序之间进行通信。
- 渲染进程:Electron.js 应用程序中的每个窗口都运行自己的渲染进程,负责使用 Chromium 渲染应用程序的用户界面。渲染进程在沙盒环境中运行,并对本机操作系统 API 的访问有限。它们通过 Electron.js 提供的 IPC 机制与主进程进行通信。
因此,当不在同一进程中时,会涉及跨进程通信。在 Electron 中,可以使用以下方法实现主进程和渲染进程之间的通信。
- 可以通过 IPC 方法在应用程序的后端(ipcMain)和前端应用程序窗口(ipcRenderer)之间实现通信,使用 ipcMain 和 ipcRenderer 模块,这些模块是进程间通信的事件触发器。
- 可以使用远程模块实现远程过程调用(RPC)通信。
远程模块返回的每个对象(包括函数)代表主进程中的一个对象(称为远程对象或远程函数)。当您调用远程对象的方法、调用远程函数或使用远程构造函数创建新对象时,它实际上会发送同步的进程间消息。
Electron 应用程序的后端和前端之间的任何状态共享(反之亦然)都是通过 ipcMain 和 ipcRenderer 模块完成的。这样,主进程和渲染进程的 JavaScript 上下文保持独立,但数据可以明确地在它们之间传输。
VSCode 的进程结构#
VSCode 具有多进程架构,启动时主要由以下进程组成:
后台进程#
后台进程作为 VSCode 的入口点,负责管理编辑器的生命周期、进程间通信、自动更新和菜单管理。
当我们启动 VSCode 时,后台进程是第一个启动的。它读取各种配置信息和历史记录,将这些信息与主窗口 UI 的 HTML 主文件路径整合到一个 URL 中,并启动一个浏览器窗口以显示编辑器的 UI。后台进程始终监控 UI 进程的状态,当所有 UI 进程关闭时,整个编辑器退出。
此外,后台进程还打开一个本地套接字。当新的 VSCode 进程启动时,它会尝试连接到此套接字并将启动参数信息传递给它,以便现有的 VSCode 可以执行相关操作。这确保了 VSCode 的唯一性,避免了打开多个文件夹导致的问题。
编辑器窗口#
编辑器窗口进程负责显示整个 UI,这就是我们在屏幕上看到的内容。UI 完全用 HTML 编写,在这方面没有太多需要介绍的内容。
Node.js 异步 IO#
读取和保存项目文件是通过主进程中的 NodeJS API 完成的。由于所有操作都是异步的,即使是相对较大的文件,也不会阻塞 UI。IO 和 UI 在同一进程中,并使用异步操作,这确保了 IO 的性能和 UI 的响应性。
扩展进程#
每个 UI 窗口将启动一个 NodeJS 子进程作为扩展的主进程。所有扩展将在此进程中一起运行。这一设计的主要目的是避免复杂的扩展系统阻塞 UI 响应。在大多数操作系统中,显示的刷新率为每秒 60 帧,这意味着应用程序需要在 16.7 毫秒内完成所有计算和 UI 刷新。HTML DOM 的速度一直受到批评,留给 JavaScript 的时间很少。因此,为了确保 UI 的响应性,所有指令必须在如此短的时间内完成。然而,实际上,在如此短的时间内完成诸如为一万行代码上色等任务是困难的。因此,这些耗时的任务需要移到其他线程或进程中。在任务完成后,结果可以返回到 UI 进程。然而,将扩展放在单独的进程中也有明显的缺点。由于它是一个单独的进程,而不是 UI 进程,因此无法直接访问 DOM 树。实时高效地更改 UI 变得困难,并且在 VSCode 的扩展系统中几乎没有用于扩展 UI 的 API。
调试进程#
调试器扩展与普通扩展略有不同。它不会在扩展进程中运行,而是每次调试时由 UI 单独在新进程中打开。
搜索进程#
搜索是一项非常耗时的任务,VSCode 也使用单独的进程来实现此功能,以确保主窗口的效率。将耗时的任务分配给多个进程有效地保证了主进程的响应性。
VSCode 中的多个进程可以分为以下三种类型:
- 主进程:主进程负责管理应用程序的整体生命周期,包括 UI 管理、扩展管理和与渲染进程的通信。它提供了一个抽象层,屏蔽了底层平台特定的细节。
- 渲染进程:VSCode 中有两种类型的渲染进程:窗口渲染器和 Webview 渲染器。窗口渲染器负责渲染应用程序的主用户界面,包括编辑器区域、侧边栏和工具栏。Webview 渲染器则用于渲染嵌入的 Web 内容,如自定义视图和面板。
- 扩展进程:VSCode 中的扩展在与主应用程序分开的进程中运行,以防止它们阻塞 UI 线程或干扰其他扩展。每个扩展在自己的隔离进程中运行,通过 IPC 机制与主进程进行通信。
总体而言,VSCode 的多进程架构允许更好的性能、提高安全性和更可靠的操作。通过将不同的任务分配到不同的进程,VSCode 可以处理复杂的操作,而不会影响应用程序的整体响应性。此外,使用扩展进程确保扩展可以安全运行,而不会对用户体验产生负面影响。
进程间通信 (IPC)#
协议#
在 IPC 通信中,协议是最基本的。就像人与人之间的沟通需要达成一致的沟通方式(语言、手语),在 IPC 中,协议可以看作是协议。
作为一种通信能力,最基本的协议范围包括发送和接收消息。
export interface IMessagePassingProtocol {
send(buffer: VSBuffer): void; // 通过低级通信通道以 Uint8Array 格式发送消息
onMessage: Event<VSBuffer>; // 在低级通信通道上接收到消息时触发上层回调函数。
}
至于具体的协议内容,可能包括连接、断开连接、事件等。
export class Protocol implements IMessagePassingProtocol {
constructor(private sender: Sender, readonly onMessage: Event<VSBuffer>) { }
// 发送消息
send(message: VSBuffer): void {
try {
this.sender.send('vscode:message', message.buffer);
} catch (e) {
// 系统出现故障
}
}
// 关闭连接
dispose(): void {
this.sender.send('vscode:disconnect', null);
}
}
IPC 本质上是发送和接收信息的能力,为了准确通信,客户端和服务器需要在同一通道上。
通道#
至于通道,它有两个功能:一个是发起调用,另一个是监听。
/**
* IChannel 是一组命令的抽象
* 调用总是返回一个 Promise,最多有一个返回值
*/
export interface IChannel {
call<T>(command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T>;
listen<T>(event: string, arg?: any): Event<T>;
}
/**
* `IServerChannel` 是 `IChannel` 的对应物,
* 在服务器端。如果您希望处理远程承诺或事件,则应实现此接口。
*/
export interface IServerChannel<TContext = string> {
call<T>(ctx: TContext, command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T>;
listen<T>(ctx: TContext, event: string, arg?: any): Event<T>;
}
客户端和服务器#
一般来说,客户端和服务器之间的区别主要在于连接的发起端是客户端,而被连接的端是服务器。在 VSCode 中,主进程是服务器,提供各种通道和服务供订阅;渲染进程是客户端,监听服务器提供的各种通道 / 服务,也可以向服务器发送消息(例如连接、订阅 / 监听、离开等)。
客户端和服务器都需要具备发送和接收消息的能力,以便正确通信。
在 VSCode 中,客户端包括 ChannelClient
和 IPCClient
。ChannelClient
仅处理最基本的通道相关功能,包括:
- 使用
getChannel
获取通道。 - 使用
sendRequest
发送通道请求。 - 接收请求结果并使用
onResponse/onBuffer
处理它们。
// 客户端
export class ChannelClient implements IChannelClient, IDisposable {
getChannel<T extends IChannel>(channelName: string): T {
const that = this;
return {
call(command: string, arg?: any, cancellationToken?: CancellationToken) {
return that.requestPromise(channelName, command, arg, cancellationToken);
},
listen(event: string, arg: any) {
return that.requestEvent(channelName, event, arg);
}
} as T;
}
private requestPromise(channelName: string, name: string, arg?: any, cancellationToken = CancellationToken.None): Promise<any> {}
private requestEvent(channelName: string, name: string, arg?: any): Event<any> {}
private sendRequest(request: IRawRequest): void {}
private send(header: any, body: any = undefined): void {}
private sendBuffer(message: VSBuffer): void {}
private onBuffer(message: VSBuffer): void {}
private onResponse(response: IRawResponse): void {}
private whenInitialized(): Promise<void> {}
dispose(): void {}
}
同样,服务器包括 ChannelServer
和 IPCServer
,其中 ChannelServer
也仅处理与通道直接相关的功能,包括:
- 使用
registerChannel
注册通道。 - 使用
onRawMessage/onPromise/onEventListen
监听客户端消息。 - 处理客户端消息并返回请求结果的
sendResponse
。
// 服务器
export class ChannelServer<TContext = string> implements IChannelServer<TContext>, IDisposable {
registerChannel(channelName: string, channel: IServerChannel<TContext>): void {
this.channels.set(channelName, channel);
}
private sendResponse(response: IRawResponse): void {}
private send(header: any, body: any = undefined): void {}
private sendBuffer(message: VSBuffer): void {}
private onRawMessage(message: VSBuffer): void {}
private onPromise(request: IRawPromiseRequest): void {}
private onEventListen(request: IRawEventListenRequest): void {}
private disposeActiveRequest(request: IRawRequest): void {}
private collectPendingRequest(request: IRawPromiseRequest | IRawEventListenRequest): void {}
public dispose(): void {}
}
IChannelServer
的主要职责包括:
- 接收来自
protocol
的消息 - 根据消息类型处理消息
- 调用相应的
IServerChannel
处理请求 - 将响应发送回客户端
- 注册
IServerChannel
IChannelServer
直接监听来自 protocol
的消息,然后调用自己的 onRawMessage
方法处理请求。onRawMessage
将根据请求的类型调用其他方法。以基于 Promise 的调用为例,我们可以看到其核心逻辑是调用 IServerChannel
的 call
方法。
private onRawMessage(message: VSBuffer): void {
const reader = new BufferReader(message);
const header = deserialize(reader);
const body = deserialize(reader);
const type = header[0] as RequestType;
switch (type) {
case RequestType.Promise:
if (this.logger) {
this.logger.logIncoming(message.byteLength, header[1], RequestInitiator.OtherSide, `${requestTypeToStr(type)}: ${header[2]}.${header[3]}`, body);
}
return this.onPromise({ type, id: header[1], channelName: header[2], name: header[3], arg: body });
// ...
}
}
private onPromise(request: IRawPromiseRequest): void {
const channel = this.channels.get(request.channelName);
let promise: Promise<any>;
try {
promise = channel.call(this.ctx, request.name, request.arg, cancellationTokenSource.token);
} catch (err) {
// ...
}
const id = request.id;
promise.then(data => {
this.sendResponse(<IRawResponse>{ id, data, type: ResponseType.PromiseSuccess });
this.activeRequests.delete(request.id);
}, err => {
// ...
});
}
连接#
现在我们有了与通道相关的客户端部分 ChannelClient
和服务器部分 ChannelServer
,但它们需要连接才能进行通信。一个连接(Connection
)由 ChannelClient
和 ChannelServer
组成。
interface Connection<TContext> extends Client<TContext> {
readonly channelServer: ChannelServer<TContext>;
readonly channelClient: ChannelClient;
}
连接的建立由 IPCServer
和 IPCClient
处理。具体来说:
IPCClient
基于ChannelClient
,负责从客户端到服务器的简单一对一连接。IPCServer
基于ChannelServer
,负责从服务器到客户端的连接。由于服务器可以提供多个服务,因此可能会有多个连接。
export class IPCClient<TContext = string> implements IChannelClient, IChannelServer<TContext>, IDisposable {
private channelClient: ChannelClient;
private channelServer: ChannelServer<TContext>;
getChannel<T extends IChannel>(channelName: string): T {
return this.channelClient.getChannel(channelName) as T;
}
registerChannel(channelName: string, channel: IServerChannel<TContext>): void {
this.channelServer.registerChannel(channelName, channel);
}
}
export class IPCServer<TContext = string> implements IChannelServer<TContext>, IRoutingChannelClient<TContext>, IConnectionHub<TContext>, IDisposable {
private channels = new Map<string, IServerChannel<TContext>>();
private _connections = new Set<Connection<TContext>>();
get connections(): Connection<TContext>[] {}
/**
* 从远程客户端检索通道。
* 经过路由后,可以指定要调用和监听的客户端。
* 否则,在没有路由的情况下进行调用时,将随机选择一个客户端,而在没有路由的情况下进行监听时,将监听每个客户端。
*/
getChannel<T extends IChannel>(channelName: string, router: IClientRouter<TContext>): T;
getChannel<T extends IChannel>(channelName: string, clientFilter: (client: Client<TContext>) => boolean): T;
getChannel<T extends IChannel>(channelName: string, routerOrClientFilter: IClientRouter<TContext> | ((client: Client<TContext>) => boolean)): T {}
registerChannel(channelName: string, channel: IServerChannel<TContext>): void {
this.channels.set(channelName, channel);
this._connections.forEach(connection => {
connection.channelServer.registerChannel(channelName, channel);
});
}
}
服务器#
- 服务是各种业务逻辑发生的实际位置。
IServerChannel
对应于一个服务,并为ChannelServer
提供call
和listen
方法,以便调用,然后调用相应的服务执行各种业务逻辑。它实际上是服务的一个包装器。IChannelServer
负责监听通过IMessagePassingProtocol
传递的请求,然后根据请求中指定的channelName
定位相应的IServerChannel
并调用它。它还能够将执行结果返回给客户端。- IPCServer 提供了一组方法,用于注册和检索 IServerChannel,并能够通过路由机制选择与之通信的客户端。
IMessagePassingProtocol
负责以 Uint8Array 的形式传输二进制信息,并在接收到消息时通过事件通知上层。
客户端#
- 业务代码是指实现业务逻辑的代码,可能会调用
IChannel
提供的方法以发起 IPC。 IChannel
提供call
和listen
方法,供业务代码发起 IPC。IPCClient
提供了一组方法,用于注册和检索IChannel
。IMessagePassingProtocol
在服务器端具有与其对应物相同的功能。
RPC 协议#
VSCode IPC 的第二种机制基于 RpcProtocol
,用于渲染进程与扩展主机进程之间的通信。如果 VSCode 在浏览器环境中运行,则用于主线程与扩展主机 Web Worker 之间的通信。
例如,如果在主机进程初始化期间发生错误,它将通知渲染进程。代码如下:
const mainThreadExtensions = rpcProtocol.getProxy(MainContext.MainThreadExtensionService);
const mainThreadErrors = rpcProtocol.getProxy(MainContext.MainThreadErrors);
errors.setUnexpectedErrorHandler(err => {
const data = errors.transformErrorForSerialization(err);
const extension = extensionErrors.get(err);
if (extension) {
mainThreadExtensions.$onExtensionRuntimeError(extension.identifier, data);
} else {
mainThreadErrors.$onUnexpectedError(data);
}
});
当调用 mainThreadExtensions
或 mainThreadError
的方法时,就会发生 IPC。
机制如下图所示:
事件系统#
在 Visual Studio Code 中,事件被广泛使用,以促进应用程序不同部分之间的通信和数据交换。事件机制基于观察者模式,允许应用程序中的对象订阅并接收有关应用程序特定方面变化或更新的通知。
以下是 VSCode 中事件机制的一些关键特性:
- 自定义事件:VSCode 提供了一种简单灵活的方式来定义可以由应用程序中的对象发出的自定义事件。事件通常定义为扩展
Event
基类的类,并包含将传递给订阅者的任何必要数据或参数。 - 事件发射器:产生事件的对象称为 “事件发射器”。这些对象包含注册的订阅者列表,并在满足某些条件时发出事件。例如,当用户保存文件时,文本编辑器组件可能会发出 “文档已保存” 事件。
- 事件订阅者:希望接收事件通知的对象称为 “事件订阅者”。这些对象使用
on
方法向事件发射器注册,该方法接受事件的名称和处理事件的回调函数。当事件被发出时,所有注册的订阅者将接收事件及其相关数据。 - 内置事件:VSCode 还包括许多内置事件,提供有关应用程序各个方面的通知,例如工作区更改、窗口聚焦和编辑器内容更改。这些事件可以使用相关服务或对象的
on
方法进行订阅。
// 这里是事件发射器的生命周期和配置的一些方面
export interface EmitterOptions {
onFirstListenerAdd?: Function;
onFirstListenerDidAdd?: Function;
onListenerDidAdd?: Function;
onLastListenerRemove?: Function;
leakWarningThreshold?: number;
}
export class Emitter<T> {
// 一些生命周期方法和可以传入的设置
constructor(options?: EmitterOptions) {}
// 允许其他人订阅此发射器发出的事件。
get event(): Event<T> {
// 在这种情况下,将根据传入的设置在相应场景中调用相关的生命周期方法。
}
// 触发事件以通知订阅者。
fire(event: T): void {}
// 清理相关的监听器和队列。
dispose() {}
}
在 VS Code 中,事件的使用主要包括:
- 注册事件发射器。
- 向外部提供定义的事件。
- 在特定时间触发事件以通知订阅者。
class WindowManager {
public static readonly INSTANCE = new WindowManager();
// 注册事件发射器。
private readonly _onDidChangeZoomLevel = new Emitter<number>();
// 检索此发射器允许其他人订阅的事件。
public readonly onDidChangeZoomLevel: Event<number> = this._onDidChangeZoomLevel.event;
public setZoomLevel(zoomLevel: number, isTrusted: boolean): void {
if (this._zoomLevel === zoomLevel) {
return;
}
this._zoomLevel = zoomLevel;
// 当 zoomLevel 更改时触发此事件。
this._onDidChangeZoomLevel.fire(this._zoomLevel);
}
}
// 提供一个方法以便外部访问全局实例。
export function onDidChangeZoomLevel(callback: (zoomLevel: number) => void): IDisposable {
return WindowManager.INSTANCE.onDidChangeZoomLevel(callback);
}
import { onDidChangeZoomLevel } from 'vs/base/browser/browser';
let zoomListener = onDidChangeZoomLevel(() => {});
const instance = new WindowManager(opts);
instance.onDidChangeZoomLevel(() => {});
export abstract class Disposable implements IDisposable {
// 使用 Set 存储注册的事件发射器。
private readonly _store = new DisposableStore();
constructor() {
trackDisposable(this);
}
// 处理事件发射器。
public dispose(): void {
markTracked(this);
this._store.dispose();
}
// 注册事件发射器。
protected _register<T extends IDisposable>(t: T): T {
if ((t as unknown as Disposable) === this) {
throw new Error('Cannot register a disposable on itself!');
}
return this._store.add(t);
}
}
Dispose 模式主要用于资源管理。这包括通过方法调用释放对象所持有的资源,例如内存。
export interface IDisposable {
dispose(): void;
}
export class DisposableStore implements IDisposable {
private _toDispose = new Set<IDisposable>();
private _isDisposed = false;
// 释放所有注册的可释放对象并标记为已释放。
// 未来添加到此对象中的所有可释放对象将在“添加”方法中被释放。
public dispose(): void {
if (this._isDisposed) {
return;
}
markTracked(this);
this._isDisposed = true;
this.clear();
}
// 不标记为已释放的情况下释放所有注册的可释放对象。
public clear(): void {
this._toDispose.forEach(item => item.dispose());
this._toDispose.clear();
}
// 添加一个可释放对象。
public add<T extends IDisposable>(t: T): T {
markTracked(t);
if (this._isDisposed) {
// 抛出错误
} else {
this._toDispose.add(t);
}
return t;
}
}
export class Scrollable extends Disposable {
private _onScroll = this._register(new Emitter<ScrollEvent>());
public readonly onScroll: Event<ScrollEvent> = this._onScroll.event;
private _setState(newState: ScrollState): void {
const oldState = this._state;
if (oldState.equals(newState)) {
return;
}
this._state = newState;
// 当状态更改时触发事件。
this._onScroll.fire(this._state.createScrollEvent(oldState));
}
}
依赖注入#
在 VS Code 中,有许多服务为不同模块提供 API。依赖项在类的构造函数中通过带有装饰器的参数声明。调用者不需要显式实例化此服务,因为这些依赖服务将在创建时自动创建并传递给调用者。不同的服务也可以相互依赖。这大大减少了程序耦合,同时提高了可维护性。
这种解耦方法称为依赖注入,是实现控制反转的一种方式。即对象(或实体)的依赖项(可能是其他对象)被外部注入,避免了在对象内部实现依赖项的实例化过程。
首先,需要定义一个类,并在其构造函数中声明其依赖项。
class MyClass {
constructor(
@IAuthService private readonly authService: IAuthService,
@IStorageService private readonly storageService: IStorageService,
) {
}
}
构造函数中的 @IAuthService
和 @IStorageService
是两个装饰器,它们属于 JavaScript 中的一个提案,并且是 TypeScript 中的实验性特性。它们可以附加到类声明、方法、访问器和参数上。在这段代码中,它们附加到构造函数参数 authService
和 storageService
上,这些是参数装饰器。参数装饰器将在运行时被调用,并传递三个参数:
- 对于静态成员,类构造函数作为第一个参数传递,而对于实例成员,类的原型作为第一个参数传递。
- 成员的名称。
- 函数参数列表中参数的索引。
服务的装饰器和接口定义通常如下所示:
export const IAuthService = createDecorator<IAuthService>('AuthService');
export interface IAuthService {
readonly id: string;
readonly nickName: string;
readonly firstName: string;
readonly lastName: string;
requestService: IRequestService;
}
服务接口需要有具体的实现,这也可以依赖于其他服务。
class AuthServiceImpl implements IAuthService {
constructor(
@IRequestService public readonly requestService: IRequestService,
){
}
public async getUserInfo() {
const { id, nickName, firstName } = await getUserInfo();
this.id = id;
this.nickName = nickName;
this.firstName = firstName;
//...
}
}
您还需要一个服务集合,用于保存一组服务并从中创建一个容器。
export class ServiceCollection {
private _entries = new Map<ServiceIdentifier<any>, any>();
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> {
const result = this._entries.get(id);
this._entries.set(id, instanceOrDescriptor);
return result;
}
forEach(callback: (id: ServiceIdentifier<any>, instanceOrDescriptor: any) => any): void {
this._entries.forEach((value, key) => callback(key, value));
}
has(id: ServiceIdentifier<any>): boolean {
return this._entries.has(id);
}
get<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> {
return this._entries.get(id);
}
}
如前所述,容器会自动实例化对象。然而,在 VSCode 中,有些服务没有其他依赖项(例如日志服务),仅被其他服务依赖,因此可以手动实例化并注册到容器中。在这个例子中,AuthServiceImpl
依赖于 IRequestService
,需要用 SyncDescriptor
包装并保存在服务集合中。
const services = new ServiceCollection();
const logService = new LogService();
services.set(ILogService, logService);
services.set(IAuthService, new SyncDescriptor(AuthServiceImpl));
SyncDescriptor
是一个用于包装需要由容器实例化的描述符的对象。它持有对象的构造函数及其静态参数(需要直接传递给构造函数)。
export class SyncDescriptor<T> {
readonly ctor: any;
readonly staticArguments: any[];
readonly supportsDelayedInstantiation: boolean;
constructor(ctor: new (...args: any[]) => T, staticArguments: any[] = [], supportsDelayedInstantiation: boolean = false) {
this.ctor = ctor; // 服务的构造函数
this.staticArguments = staticArguments;
this.supportsDelayedInstantiation = supportsDelayedInstantiation;
}
}
现在我们可以创建一个容器并使用它注册服务。在 VSCode 中,容器是 InstantiationService
。
const instantiationService = new InstantiationService(services, true);
InstantiationService
是依赖注入的核心。在使用容器注册服务后,我们需要手动实例化程序的入口点。在 VSCode 中,这是 CodeApplication
。容器(instantiationService
)持有这些对象之间的依赖关系,因此 CodeApplication
也需要使用容器进行实例化。
// 这里,第二和第三个参数是 CodeApplication 构造函数的静态参数,需要手动传入。
instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup();
您还可以通过调用 instantiationService.invokeFunction
方法手动获取服务实例,并传入一个回调函数。回调函数将访问器作为参数,当通过访问器获取指定服务时,容器会自动分析其依赖服务并在返回服务实例之前实例化它们。
instantiationService.invokeFunction(accessor => {
const logService = accessor.get(ILogService);
const authService = accessor.get(IAuthService);
});
instantiationService
包含一个名为 createChild
的成员方法,可以创建一个子容器。为了更好地定义依赖关系,子容器可以访问父容器中的服务实例,但父容器无法访问子容器中的实例。当所需的服务实例在子容器中不存在时,它会调用 instantiationService._parent
来获取对父容器的引用,并向上递归查找依赖项。
参考#
Electron (stephanosterburg.com)
VSCode 技术解析 | VSCode 技术解析与实践 (codeteenager.github.io)
Visual Studio Code / Egret Wing 技术架构:基础・Chen's Blog (imzc.me)
VSCode 源码解读:事件系统设计 | 被删的前端游乐场 (godbasin.github.io)
https://zhuanlan.zhihu.com/p/60228431
https://zhuanlan.zhihu.com/p/96041706
vscode 源码解析 - 依赖注入 - 知乎 (zhihu.com)