Lumino 的 disposable 与组件生命周期:东西造出来,总要有人负责善后
前面在讲 Widget / signaling / messaging 的时候,其实一直有个“隐身角色”:
谁来负责把事件监听、定时器、模型订阅这些东西在合适的时机清掉?
这篇就从@lumino/disposable讲起,顺便把几类常见组件的生命周期串一遍。
@lumino/disposable:一个很小但到处都在用的模式
@lumino/disposable 暴露的大致内容很简单:
IDisposable接口:只有一个dispose(): void方法;DisposableDelegate:包装一个() => void的小工具,dispose()时调用这个函数;DisposableSet:一组 disposable 的集合,可以一次性dispose()全部。
最朴素的用法是:
1import { DisposableDelegate, DisposableSet } from '@lumino/disposable';
2
3// 单个 disposable:包一段“清理逻辑”
4const d1 = new DisposableDelegate(() => {
5 console.log('clean up something');
6});
7
8// disposable 集合:方便成批管理
9const bag = new DisposableSet();
10bag.add(d1);
11bag.add(new DisposableDelegate(() => console.log('another cleanup')));
12
13// 在合适的时机统一释放
14bag.dispose();
看起来非常简单,但它的价值在于——给“资源释放”这件事一个统一的抽象,
在大型框架(比如 Theia)里,很多服务/组件都会实现 IDisposable,方便在应用关闭或容器销毁时统一清理。
把 disposable 和 Widget 生命周期结合起来
以 Widget 为例,一个常见套路是:
- 在
onAfterAttach里订阅事件、signal、定时器等; - 在
onBeforeDetach或 Widget 自身的dispose()里用DisposableSet把这些资源一次性释放。
一个简单的示例:
1import { Widget } from '@lumino/widgets';
2import { DisposableSet, DisposableDelegate } from '@lumino/disposable';
3import { ISignal, Signal } from '@lumino/signaling';
4
5class Model {
6 private _changed = new Signal<Model, void>(this);
7
8 get changed(): ISignal<Model, void> {
9 return this._changed;
10 }
11
12 trigger() {
13 this._changed.emit(void 0);
14 }
15}
16
17class MyWidget extends Widget {
18 private _model = new Model();
19 private _toDispose = new DisposableSet();
20
21 constructor() {
22 super();
23 this.addClass('my-DisposableDemo');
24 }
25
26 protected onAfterAttach(msg: any): void {
27 // 1. 订阅 model 的 signal
28 this._model.changed.connect(this.onModelChanged, this);
29 this._toDispose.add(
30 new DisposableDelegate(() => {
31 this._model.changed.disconnect(this.onModelChanged, this);
32 }),
33 );
34
35 // 2. 绑定 DOM 事件
36 const handler = () => this._model.trigger();
37 this.node.addEventListener('click', handler);
38 this._toDispose.add(
39 new DisposableDelegate(() => {
40 this.node.removeEventListener('click', handler);
41 }),
42 );
43 }
44
45 protected onBeforeDetach(msg: any): void {
46 // 3. 在离开 DOM 前统一清理
47 this._toDispose.dispose();
48 }
49
50 private onModelChanged(sender: Model): void {
51 console.log('model changed');
52 }
53}
这段代码表达的意思很简单:
只要 Widget 不再挂在页面上,它附带的各种监听/订阅都应该跟着寿终正寝,而不是在后台默默泄露。
在 Theia 里也有类似的模式,只不过很多时候是通过依赖注入 + DisposableCollection(和 Lumino 很像)来管理。
常见组件的生命周期:从 Application 到 Widget
顺着 disposable 的话题,把几种常见层级的生命周期按“自上而下”捋一下(简化版,只讲和清理相关的点):
1. Application 层
- 创建阶段:
- new
Application/FrontendApplication; - 构造
CommandRegistry、Shell、菜单系统等; - 注册各种服务、贡献点(在 Theia 里是 Contribution)。
- new
- 运行阶段:
- 通过命令、菜单、布局等创建/销毁一批批 Widget / 视图。
- 销毁阶段:
- 应用关闭时,调用 Application 的
dispose(),它再递归调用 shell / 服务等的dispose()。
- 应用关闭时,调用 Application 的
这里 @lumino/disposable 的作用是给“服务”和“子系统”一个统一的离场接口,便于 Application 在 shutdown 时不遗漏。
2. Shell / 布局层
以包含 DockPanel 的 Shell 为例:
- 创建/attach:DockPanel 被挂到 DOM 上,子 Widget 依次收到
onAfterAttach。 - 布局变化:拆分/合并/关闭标签,部分 Widget 被从 DockPanel 中移除,触发
onBeforeDetach。 - 彻底销毁:Shell 自身被
dispose(),通常会把内部所有 Widgetdispose()一遍。
在这个层级上,比较重要的是:DockPanel 自己也实现了 dispose(),会清理内部的布局状态和监听,
不然长时间折腾布局可能会造成内存堆积。
3. 单个 Widget 层
对于一个普通 Widget,生命周期大概是这样的:
- 构造函数:初始化状态,但不要操作 DOM(因为还没 attach)。
onAfterAttach:- 可以安全地访问
this.node所在的 DOM 环境; - 适合绑定事件、启动定时器、订阅模型 signal。
- 可以安全地访问
onUpdateRequest/onResize:- 响应外界的更新/布局变化;
- 通常通过
this.update()触发。
onBeforeDetach:- 从 DOM 中卸载前最后的机会;
- 适合解除事件监听、取消定时任务、断开 signal 订阅(通常配合
DisposableSet)。
dispose():- Widget 生命周期的最终终点;
- 会确保不再接受消息循环,内部资源应在这里全部释放。
在 Theia 的 ReactWidget 等封装里,这套生命周期会再被转译成更贴近 React 的钩子,但底层还是 Lumino 的消息/生命周期模型在运转。
在 Theia/Lumino 里看“生命周期 + 资源释放”的几个观察点
实际翻代码或写扩展时,我自己会刻意留意这些地方:
- 有没有实现/继承某个
IDisposable/DisposableCollection:- 有的话,基本可以推断这个对象在某处会被集中
dispose(); - 自己往里面加资源(比如
toDispose.push(...))就比较安全。
- 有的话,基本可以推断这个对象在某处会被集中
- Widget 是否在
onBeforeDetach或dispose()里对事件/信号做了对称的清理:- 如果只在
onAfterAttach里addEventListener或connect,没有拆,就要小心可能的泄露。
- 如果只在
- 服务/单例对象里有没有长生命周期的订阅:
- 比如 singleton service 订阅了很多 view/model 的事件,却从不释放,这种在 IDE 跑久了很容易炸内存。
把这些模式装进脑子之后,再看 Theia / Lumino 源码里各种“清理逻辑”,会觉得亲切很多:
大多数看起来“啰嗦”的 dispose 代码,其实都是在给长跑型应用买安全感。
小结:disposable 是“看不见,但处处在”的一层保障
总结一下这一篇想说的:
@lumino/disposable本身非常小,但给“资源释放”提供了一个统一接口和组合工具;- 把它和 Widget / Shell / Application 等不同层级的生命周期结合起来,可以形成一套相对清晰的“谁负责善后”的约定;
- 在像 Theia 这种长时间运行的 IDE 场景里,这种模式对避免内存泄露、事件乱飞有很现实的意义。
写这一篇更像是给自己加一个过滤器:
以后看到 dispose()、Disposable*、onBeforeDetach 这些字样时,脑子里会自动敲个钟——这里是在讲“善后”,值得多看两眼。