在 Lesson 0001 中我们解剖了 package.json——扩展的"宣言"。这一课我们打开 extension.ts,追踪 VS Code 实际加载扩展时发生了什么:从 activate() 被调用,到 deactivate() 被清理。
VS Code 在满足 activationEvents 条件时,执行入口文件的 activate() 函数。它传入一个 ExtensionContext 对象——这是扩展与 VS Code 之间的桥梁。
import * as vscode from 'vscode'; export function activate(context: vscode.ExtensionContext) { // context 是 VS Code 给你的工具箱 // 最重要的工具:context.subscriptions }
| 属性/方法 | 类型 | 用途 |
|---|---|---|
| subscriptions | Disposable[] | 推送需要自动清理的资源(命令、监听器、定时器……) |
| extensionPath | string | 扩展的安装目录(只读文件) |
| globalState | Memento | 跨会话持久化存储(全局) |
| workspaceState | Memento | 跨会话持久化存储(每工作区) |
| secrets | SecretStorage | 安全存储敏感信息(API key 等) |
subscriptions 是 VS Code 扩展中最重要的资源管理模式。把它想象成一个"自动清理清单"——你注册命令、绑定事件监听器时,把返回的 Disposable 对象 push 进这个数组。当扩展被停用时,VS Code 会自动调用每个对象的 dispose() 方法。你不需要手动逐一清理。
你可能会想:既然每个 registerXxx() 都返回 Disposable,为什么 VS Code 不自动把它们加到 subscriptions 里?为什么要开发者手动 push?这触及了 VS Code Extension API 的设计哲学核心。
// registerCommand 是 vscode.commands 上的静态方法 // 它不在 activate() 内部,根本访问不到 context const disposable = vscode.commands.registerCommand('my.command', callback); // context 只在 activate(context) 的参数里出现 // 两者在代码结构上是完全分离的
registerCommand() 是挂载在 vscode.commands 命名空间上的全局静态方法,它不知道——也不应该知道——当前的 ExtensionContext 在哪里。要把它们耦合在一起,API 需要改头换面(比如让每个 register 方法接受一个可选的 context 参数),但 VS Code 团队刻意没这么做。
不是所有 Disposable 都应该活到扩展停用。有时你需要中途销毁某个资源:
// 场景:切换工作区时重建监听器 let workspaceWatcher: vscode.Disposable; function setupWorkspace() { workspaceWatcher?.dispose(); // 先清理旧的 workspaceWatcher = vscode.workspace.onDidChangeTextDocument(handleChange); // 这个监听器不应该进 subscriptions——我们要在切换工作区时手动 dispose }
如果 VS Code 自动把所有东西塞进 subscriptions,你就失去了中途清理的能力——资源只能等扩展停用才释放。这会浪费内存,甚至因为持有了已失效的引用而出错。
"创建资源的人负责销毁它。"
——Dispose Pattern(源自 .NET / RxJS 社区)
registerCommand() 创建了一个命令注册,但它把所有权以 Disposable 对象的形式转移给你。你可以选择:
subscriptions——让它随扩展一起消亡.dispose()——中途取消vscode.Disposable.from(d1, d2, d3) 合并管理多个 Disposable自动管理会剥夺这种选择权。手动 push 不是负担,而是赋权。
fs.watch() 返回的是 Node.js 的 FSWatcher,不是 VS Code 的 Disposable。VS Code 不可能自动管理它不认识的资源类型。但通过适配器模式——包一层 { dispose: () => watcher.close() }——你可以把任意资源包装成 VS Code 约定,享受统一的清理待遇。
subscriptions 不是 VS Code "帮不了你"的妥协,而是把资源生命周期的控制权交给你。这是一种赋权,不是负担。
打开 temp_repo/src/extension.ts 第 36 行,activate() 做了五件事,按照顺序:
const hideCommand = vscode.commands.registerCommand( 'claudeContextBar.hideSession', (sessionFile: string) => { hiddenSessions.set(sessionFile, Date.now()); refreshAllSessions(); } ); context.subscriptions.push(hideCommand); // ⬅ 关键:推入清理清单
registerCommand() 返回一个 Disposable。如果不 push 到 subscriptions,扩展停用后命令引用会泄漏——用户点击状态栏上的隐藏按钮时 VS Code 会尝试调用一个已经不存在的回调。
| 用户命令 | 内部命令 | |
|---|---|---|
| 声明位置 | package.json → contributes.commands | 只在代码中 registerCommand() |
| 命令面板 | ✅ 出现在 Ctrl+Shift+P | ❌ 不出现 |
| 快捷键 | ✅ 可绑定 | ❌ 不可绑定 |
| 典型用途 | 用户主动触发的操作 | UI 组件的点击回调、程序间通信 |
claudeContextBar.hideSession 是内部命令——你在 package.json 里找不到它,因为它不是给用户在命令面板里调用的。它存在的唯一目的是:当用户点击状态栏条目时,VS Code 执行对应的回调。用户点击状态栏条目 → VS Code 读取 StatusBarItem.command → 按 ID 找到 registerCommand 注册的回调 → 传入 arguments 作为参数 → 回调执行。vscode.commands.executeCommand('claudeContextBar.hideSession', ...)package.json 的 contributes.commands 中声明了package.json 中声明才能在 keybindings 中使用package.json 声明 hideSession 是一种有意为之:用户不应该在命令面板中看到 "Hide Session" 这个选项(他们不知道 session 是什么),这个命令只作为状态栏点击的"技术回调"存在。声明式配置管用户界面,命令式注册管内部逻辑——各司其职。
const configWatcher = vscode.workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration('claudeContextBar')) { refreshAllSessions(); // 用户在 settings.json 中改了配置,立即刷新 } }); context.subscriptions.push(configWatcher);
registerSomething() 和 onDidSomething() 方法都返回 Disposable。你负责把它 push 到 context.subscriptions,VS Code 负责在停用时调用 dispose()。简单、统一、不容易遗漏。
refreshAllSessions(); // 立即扫描一次,生成初始状态栏 UI
这是 activate() 中的唯一一个"做事"的调用——前面的注册只是"设置规则",这里的 refreshAllSessions() 才是实际干活:扫描 Claude Code 的会话文件、解析 token 用量、创建状态栏条目。
const claudeProjectsDir = getClaudeProjectsDir(); if (fs.existsSync(claudeProjectsDir)) { fileWatcher = fs.watch(claudeProjectsDir, { recursive: true }, (event, filename) => { if (filename?.endsWith('.jsonl')) { refreshAllSessions(); // 会话文件变化时刷新 } } ); }
Node.js 的 fs.watch() 直接监听文件系统。每当 Claude Code 写入新的会话数据,.jsonl 文件发生变化,扩展立刻刷新状态栏。注意这里 fileWatcher 没有 被 push 到 subscriptions——因为它不是 VS Code Disposable,而是 Node.js 的 FSWatcher 对象,需要在 deactivate() 中手动 .close()。
const config = vscode.workspace.getConfiguration('claudeContextBar'); const intervalSeconds = config.get<number>('refreshInterval', 30); refreshInterval = setInterval(refreshAllSessions, intervalSeconds * 1000);
文件监听不能覆盖所有情况(比如网络文件系统、某些编辑器的写入方式),所以扩展设置了一个兜底定时器。这是工程中的常见模式——用轮询弥补事件驱动的盲区。
除了 push 单个 Disposable,context.subscriptions 还可以直接 push 一个包含 dispose() 方法的对象。claude-context-bar 在 activate 末尾用了这个技巧:
context.subscriptions.push({
dispose: () => {
if (fileWatcher) { fileWatcher.close(); }
if (refreshInterval) { clearInterval(refreshInterval); }
statusBarItems.forEach(entry => entry.item.dispose());
statusBarItems.clear();
}
});
这使得所有需要清理的资源(Node.js watcher、定时器、状态栏条目)都被统一在一个 dispose() 函数中,享受和 VS Code Disposable 相同的自动清理待遇。这是适配器模式——把非 VS Code 的资源包装成 VS Code 的 Disposable 契约。
deactivate() 或 subscriptions 的 dispose 中清理,扩展停用后它仍然在运行。这会导致内存泄漏、CPU 空转,甚至因为持有了对已销毁 VS Code 对象的引用而崩溃。
export function deactivate() { if (fileWatcher) { fileWatcher.close(); } if (refreshInterval) { clearInterval(refreshInterval); } statusBarItems.forEach(entry => entry.item.dispose()); statusBarItems.clear(); }
claude-context-bar 的 deactivate() 和 subscriptions 中那个 dispose 函数几乎一模一样。实际上,在正常流程下这是冗余的——因为两者都会在扩展停用时被调用。
// VS Code 停用扩展时的执行顺序: deactivate() 被调用 → fileWatcher.close() → clearInterval(refreshInterval) → statusBarItems dispose ↓ context.subscriptions 逐个 dispose → 包装对象 dispose() 被调用 → fileWatcher.close() ← 第二次调用 → clearInterval(refreshInterval) ← 第二次调用 → statusBarItems dispose ← 第二次调用
好在这些操作(close()、clearInterval()、dispose())重复调用通常是无害的 no-op——不会崩溃,只是多余。
deactivate()——这是教程教会每个人的标准模式这种"防御性编程"在工程中很常见,但它防御的场景其实不存在——如果 VS Code 连 deactivate() 都不调用,那它大概率也顾不上 dispose subscriptions 了。
| 方案 | 优点 | 风险 |
|---|---|---|
A. 全押 subscriptions(推荐)context.subscriptions.push(disposable) | 每注册一个资源立刻 push,不会漏;省去写 deactivate() 的心智负担 | 需要手动中途 dispose 的资源要自己持有引用 |
| B. 全在 deactivate 手动清理 不 push 到 subscriptions | 所有清理逻辑集中一处,便于审查 | 容易漏:你必须在 activate 和 deactivate 之间维护"注册了什么"的对应关系 |
两者不要同时写——那是没有收益的心智负担。如果你不确定选哪个,方案 A 是 VS Code 社区的默认做法。
| 时刻 | 发生什么 |
|---|---|
| VS Code 启动完成 | onStartupFinished 激活事件触发 |
| 加载扩展 | VS Code 执行 ./out/extension.js,调用 activate(context) |
| 注册命令 | registerCommand('claudeContextBar.hideSession', …) |
| 绑定监听 | onDidChangeConfiguration(…) —— 用户改设置时响应 |
| 首次扫描 | refreshAllSessions() —— 生成初始状态栏 UI |
| 设置文件监听 | fs.watch(…) —— 会话文件变化时刷新 |
| 启动定时器 | setInterval(refreshAllSessions, …) —— 兜底轮询 |
| 运行时循环 | 状态栏持续更新 ← 文件监听 + 定时器 + 配置变化 |
| VS Code 关闭 / 扩展被禁用 | subscriptions 全部 dispose → deactivate() 调用 → 扩展退出 |
activate() 函数(第 36 行)activate() 执行的五个步骤,不看书subscriptions 了吗?如果没有,它在哪里被清理?refreshAllSessions() 函数(第 433 行起),快速浏览它的主干逻辑——不要求完全读懂,只需识别它的输入和输出在 activate() 中通过 registerCommand() 注册的命令,它的 Disposable 被 push 到了哪里?
claude-context-bar 的 fs.watch() 文件监听没有被直接 push 到 context.subscriptions。它在哪里被清理?
为什么 claude-context-bar 同时设置了 fs.watch() 文件监听和 setInterval() 定时器?
VS Code 为什么不在 registerCommand() 内部自动帮你把 Disposable 添加到 subscriptions?
Disposable 模式、为什么资源管理这么重要、或者 activate 中任何一段代码有疑问,直接在这个对话中提问。