我不知道的 VSCode 扩展(05)— 生命周期与激活策略
VSCode 可以装几百个扩展而启动速度不崩,靠的不是硬件,而是延迟激活机制。
一、扩展不是”一启动就全跑”的
VSCode 可以装几百个扩展而启动速度不崩,靠的不是硬件,而是延迟激活机制。
很多人以为 VSCode 一启动,所有扩展就全部加载运行了。但实际上,绝大多数扩展在 VSCode 启动后处于”未激活”状态——它们的 package.json 被 VSCode 解析了(所以命令、菜单、视图已经可见),但 extension.ts 中的 activate() 函数还没有执行。
只有当特定条件满足时,VSCode 才会去加载和执行扩展代码。这个”特定条件”就是 Activation Events(激活事件)。
二、激活事件:显式声明 vs 隐式推断
显式声明
在 package.json 的 activationEvents 数组中明确写出触发条件:
{
"activationEvents": ["onLanguage:python", "onView:myTreeView", "onStartupFinished"]
}
常见的激活事件:
| 事件 | 触发时机 |
|---|---|
onCommand:xxx | 命令被执行时 |
onLanguage:xxx | 打开某种语言的文件时 |
onView:xxx | 某个视图变得可见时 |
onFileSystem:xxx | 访问某种文件系统方案时 |
onWebviewPanel:xxx | 某种 Webview 面板被创建时 |
onStartupFinished | VSCode 启动完成后(所有窗口初始化完毕) |
onTerminal | 终端被打开时(2025.7 新增) |
workspaceContains:xxx | 工作区包含匹配的文件时(如 **/*.py) |
* | 立即激活(强烈不推荐) |
隐式推断(VSCode 1.74+)
这里有一个很多人会忽略的细节——从 VSCode 1.74 开始,很多激活事件不需要手动声明了。VSCode 会根据扩展在 contributes 中的声明自动推断:
| contributes 中声明了 | VSCode 自动推断的激活事件 |
|---|---|
contributes.commands 中有命令 A | onCommand:A |
contributes.languages 中注册了语言 L | onLanguage:L |
contributes.views 中注册了视图 V | onView:V |
contributes.customEditors | 打开关联文件时 |
contributes.authentication | 身份验证触发时 |
换句话说,如果扩展的唯一激活场景是”用户执行扩展注册的命令”,那 activationEvents 可以留空数组:
{
"activationEvents": [],
"contributes": {
"commands": [{ "command": "my-ext.doSomething", "title": "Do Something" }]
}
}
VSCode 会自动在 my-ext.doSomething 被执行时激活扩展。
但要注意:如果扩展需要在”没有用户主动操作”的情况下激活(比如启动后自动检测工作区配置),就必须显式声明 onStartupFinished 或 workspaceContains,因为这些场景无法从 contributes 推断。
* 通配符:能用但不该用
* 表示”VSCode 一启动就激活”。听起来方便,但代价是拖慢所有用户的启动速度。VSCode 官方文档明确建议不要使用 *,除非扩展确实需要在启动时立即工作。
从 Running Extensions 视图(命令面板输入 Developer: Show Running Extensions)可以看到每个扩展的激活时间。如果一个扩展的激活时间超过 200ms,就值得考虑优化激活策略。
三、activate() — 扩展的启动入口
当激活条件满足时,VSCode 会加载扩展的入口模块并执行 activate() 函数:
export function activate(context: vscode.ExtensionContext) {
// 注册命令
context.subscriptions.push(
vscode.commands.registerCommand('my-ext.doSomething', () => {
// 命令逻辑
}),
);
// 注册事件监听
context.subscriptions.push(
vscode.workspace.onDidSaveTextDocument((doc) => {
// 保存时执行
}),
);
}
context 参数是 ExtensionContext 类型,是扩展和 VSCode 之间的桥梁。它提供几个关键能力:
| 属性/方法 | 作用 |
|---|---|
subscriptions | 可销毁资源的收集器,扩展卸载时自动 dispose |
extensionUri | 扩展安装目录的 URI |
extensionPath | 扩展安装目录的绝对路径 |
globalState | 跨工作区持久化存储(类似 localStorage) |
workspaceState | 工作区级持久化存储 |
secrets | 安全存储(用于 token 等敏感信息) |
globalStorageUri | 全局存储目录的 URI |
subscriptions:资源管理的关键
问题的关键在于——context.subscriptions 是防止内存泄漏的核心机制。
VSCode 中几乎所有资源都实现了 Disposable 接口。命令、事件监听器、状态栏项目、文件监视器——注册时返回一个 disposable 对象,调用 .dispose() 可以释放资源。
把 disposable 推入 context.subscriptions 数组后,扩展卸载时 VSCode 会自动逐一调用 .dispose(),释放所有资源。
如果不加入 subscriptions,就必须在 deactivate() 中手动释放,否则会泄漏。
下面这段代码能说明问题:
// 错误写法:泄漏
export function activate(context: vscode.ExtensionContext) {
vscode.workspace.onDidSaveTextDocument((doc) => {
// 这个监听器永远不会被释放
});
}
// 正确写法
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.workspace.onDidSaveTextDocument((doc) => {
// 扩展卸载时自动释放
}),
);
}
globalState 和 workspaceState:轻量持久化
// 写入
context.globalState.update('lastRun', Date.now());
// 读取
const lastRun = context.globalState.get<number>('lastRun');
globalState 跨工作区共享,workspaceState 只在当前工作区生效。底层是 SQLite 存储,适合存少量配置数据,不适合存大文件。
四、deactivate() — 扩展的关闭收尾
export function deactivate() {
// 释放 subscriptions 无法覆盖的资源
// 保存未持久化的状态
}
deactivate() 是可选的。在以下时机被调用:
(1) VSCode 窗口关闭
(2) 扩展被禁用或卸载
(3) VSCode 重新加载窗口(Developer: Reload Window)
很多人以为”只要用了 context.subscriptions 就不需要 deactivate()”,大多数情况下确实如此。但有一种场景必须用 deactivate()——异步清理。
如果扩展在运行时创建了需要异步关闭的资源(比如一个 WebSocket 连接或一个子进程),deactivate() 可以返回一个 Promise,VSCode 会等待这个 Promise 完成(但有超时限制):
let ws: WebSocket | undefined;
export function activate(context: vscode.ExtensionContext) {
ws = new WebSocket('ws://localhost:8080');
}
export function deactivate(): Promise<void> {
return new Promise((resolve) => {
if (ws) {
ws.close();
ws.on('close', resolve);
} else {
resolve();
}
});
}
说白了,context.subscriptions 处理同步资源的释放,deactivate() 处理异步资源的关闭。
五、完整生命周期时序
VSCode 启动
↓
解析所有扩展的 package.json(声明层加载)
↓
渲染已注册的命令、菜单、视图骨架
↓
用户触发某个激活事件(如执行命令)
↓
VSCode 加载扩展入口模块
↓
执行 activate(context)
↓
扩展进入运行状态,响应用户操作
↓
VSCode 关闭 / 扩展被禁用
↓
执行 deactivate()
↓
释放 context.subscriptions 中的资源
↓
扩展生命周期结束
如果你只记住一句话:声明在安装时解析,代码在激活时执行,资源在卸载时释放——这是 VSCode 扩展的三段式生命周期。
本系列其他文章:
- 上一篇:API 全景与实战技巧
- 下一篇:调试与测试
延伸阅读: