建築風格與原則#
VS Code 採用分層和模組化的方法來設計其核心套件。核心套件旨在使用擴展 API 進行擴展,該 API 提供了 VS Code 中幾乎所有可用的功能。這種架構風格有助於將代碼編輯器的不同責任分開,以便於實現和測試,讓開發者清楚知道他們應該在哪裡實現特定的功能部分。
每一層都與其下方的層直接通信。基礎層通過在基礎層中定義的平台特定依賴項與平台層進行通信,而平台層則通過擴展 API 與擴展層進行通信。在接下來的部分中,我們將主要集中於對 VS Code 的基礎層、平台層和擴展層的分析。
基礎層#
基礎層建立在 ElectronJS 框架之上,提供可以在任何其他層中使用的一般實用工具和用戶界面構建塊。這些組件是使用 HTML、CSS 和 JavaScript 等網頁技術構建的。基礎層還提供文件系統實用工具,允許 VS Code 與文件系統交互,例如讀取和寫入文件、導航目錄以及創建新文件和目錄。更重要的是,這一層包括擴展管理系統,允許用戶安裝、管理和更新擴展。它是使用網頁技術和 ElectronJS API 構建的,並提供了一個直觀的界面來管理擴展。
基礎包包括幾個子包,例如 common、electron-browser 和 node。這些子包中的每一個都包含特定於該包的模組。common 包包括在不同平台和環境中使用的模組。這包括處理文本緩衝區、處理文件和目錄以及提供基本 UI 組件(如按鈕和菜單)的模組。electron-browser 包包括特定於 ElectronJS 平台的模組,該平台用於在桌面上運行 VS Code。這包括處理 UI 元素、管理網頁視圖和與 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 設計為高度可擴展,使用網頁技術和 VS Code 擴展 API。這使得開發者可以通過擴展輕鬆地向編輯器添加新功能和功能。
-
靈活性:VS Code 的設計也很靈活,能夠支持多種編程語言、開發環境和平台。它建立在網頁技術和原生代碼的組合上,允許它在多個操作系統上運行,並且可以輕鬆自定義以滿足不同開發者的需求。
在 VS Code 的設計中還採用了其他架構原則,包括關注點分離、模組化和分層架構。這些原則有助於確保系統易於理解、維護和擴展。
Electron#
- 使用網頁技術編寫 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 的擴展系統中幾乎沒有 API 用於擴展 UI。
調試進程#
調試器擴展與普通擴展略有不同。它不在擴展進程中運行,而是每次調試時由 UI 單獨在新進程中打開。
搜索進程#
搜索是一項非常耗時的任務,VSCode 也使用單獨的進程來實現此功能,以確保主窗口的效率。將耗時的任務分配給多個進程有效地保證了主進程的響應性。
VSCode 中的多個進程可以分為以下三種類型:
- 主進程:主進程負責管理應用程序的整體生命周期,包括 UI 管理、擴展管理和與渲染進程的通信。它提供了一個抽象層,將底層平台特定的細節與應用程序的其餘部分隔離。
- 渲染進程:VSCode 中有兩種類型的渲染進程:窗口渲染器和網頁視圖渲染器。窗口渲染器負責渲染應用程序的主要用戶界面,包括編輯區、側邊欄和工具欄。網頁視圖渲染器則用於渲染嵌入的網頁內容,如自定義視圖和面板。
- 擴展進程: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 在瀏覽器環境中運行,則用於主線程和擴展主機網頁工作者之間的通信。
例如,如果在主機進程初始化期間發生錯誤,它將通知渲染進程。代碼如下:
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;
// 釋放所有已註冊的 Disposable 並標記為已釋放。
// 將來添加到此對象的所有 Disposable 將在 'add' 方法中被釋放。
public dispose(): void {
if (this._isDisposed) {
return;
}
markTracked(this);
this._isDisposed = true;
this.clear();
}
// 釋放所有已註冊的 Disposable 而不標記為已釋放。
public clear(): void {
this._toDispose.forEach(item => item.dispose());
this._toDispose.clear();
}
// 添加一個 Disposable。
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)