type
status
date
slug
summary
tags
category
icon
password
VS Code 之所以封装
DomEmitter(准确叫法是 DOMEmitter / DomEmitter / DOMEvent 系列工具),核心原因不是为了简单监听 DOM 事件,而是为了让:DOM 事件也能进入 VS Code 内部统一的事件系统(Event + IDisposable)中。
换句话说:
🌟 DomEmitter 的作用
把 “原生 DOM 事件(KeyboardEvent、MouseEvent...)” → 转换成 “VS Code 内部 Event 事件流”。
为什么 VS Code 不直接用原生 addEventListener?
因为 DOM 事件有几个问题:
❌ 1. 不是类型化的
DOM 事件都是浏览器定义的结构:
MouseEvent, KeyboardEvent…VS Code 的内部组件想处理的是自己的类型系统:
IKeyboardEvent
IMouseTarget
IEditorMouseEvent
IEditorKeydownEvent
IStandardKeyboardEvent
它们常常需要 normalize、抽象、扩展,比如:
- 是否
Ctrl/Meta应同时判断(Win/Mac 平台差异)
- 暴露更高层结构:比如点击位置 → 转换为行列(Position)
- 把浏览器事件包装成可组合的
Event<T>
DOM 事件不能直接满足这些需求。
❌ 2. 不能用 VS Code 的 disposable 机制
VS Code 的组件生命周期统一是:
DOM API 相当丑陋:
一旦 anonymous listener,很难安全移除 → 内存泄漏。
VS Code 想做到:
- 每个组件销毁时只需执行
dispose()
- 所有 DOM 事件、模型事件、UI 事件一次性全部注销
DOM 自带的事件并没有这个能力。
❌ 3. 不能被组合(map/filter/debounce/once)
VS Code 的
Event<T> 是一个“事件流”,可以:DOM 事件不能 map/filter,只能回调。
❌ 4. 浏览器差异太大,需要统一封装
例如:
- 不同浏览器 keyCode 差异
- Mac 上快捷键是 metaKey,而 Windows 是 ctrlKey
- 鼠标行为、右键行为在跨平台上不一致
- IME 输入在某些浏览器下会触发不同事件组合
VS Code 需要统一抽象成:
StandardKeyboardEvent
StandardMouseEvent
EditorMouseEvent
这些都需要在事件进入逻辑层之前先统一格式。
所以它封装了 DomEmitter
核心代码很简单,但思想很强:
它做的就是:
- listen DOM event
- fire into VS Code Event stream
- return IDisposable
- auto cleanup on dispose()
实际用途:举例说明
👇 假设你在 editor 里想拿到鼠标点击的位置(行/列)
❌ DOM 事件能给你啥?
你还得自己算:
- 点击的是哪一行?
- 哪一列?
- 是否点在 gutter?
- 是否点在折叠箭头?
非常痛苦。
✅ VS Code 事件能给你:
这些都是基于 DomEmitter + 自己的抽象做出来的。
总结:为啥有 DomEmitter?
需求 | DOM Event | DomEmitter + Event |
类型安全 | ❌ 无 | ✅ 事件载荷顶层统一 |
生命周期管理 | ❌ removeListener 很麻烦 | ✅ IDisposable 一键清理 |
事件组合 | ❌ 不支持 | ✅ map/filter/debounce/once |
跨平台差异 | ❌ 浏览器差异巨大 | ✅ 在 fire 前统一封装结构 |
高层抽象(行列、鼠标目标) | ❌ 自己计算 | ✅ 事件包装器自动转换 |
和 VS Code 内部 API 一致 | ❌ 无法统一 | ✅ 同一 Event 系统 |
一句话总结:
VS Code 封装 DOM 事件,是为了把浏览器事件 “抽象成可组合、可管理、可类型化、可跨平台的 VS Code 内部事件系统的一部分”。
总结一下这套 event 架构的优势,和之前的回调方式有什么不同?
先说一句大白话
- 之前:
「DOM 事件 → 回调函数」,谁关心就自己
addEventListener,自己清理。- 现在:
「DOM 事件 → DomEmitter → Event 流 → 各层组件订阅」,所有事件都变成统一类型的、可组合的、可管理的“事件流”。
这两种方式在简单 demo 里差不多,但一旦系统变大(像 VS Code),差异就非常明显。
一、类型 & 抽象层级上的优势
1. 有统一抽象:Event<T> 而不是裸回调
- 之前:
这是DOM 专用的签名,其他模块没法复用这个模式。
- 现在:
任何事件,不管来自 DOM、Model 还是 Editor 自己,都是
Event<T> 这一个抽象。好处:
- 不管是键盘、鼠标还是内容变更,一律同一种调用方式:
event(listener) -> IDisposable- 更容易做通用工具(map/filter/once/debounce)——这在简单回调模型里做不到。
2. 把“平台事件”转成“领域事件”
你现在的链路是:
- DOM:
KeyboardEvent / MouseEvent / UIEvent
- →
DomEmitter<T>:接入到内部事件系统
- →
StandardKeyboardEvent / StandardMouseEvent:统一 ctrl/cmd、坐标、按钮…
- → 编辑器领域事件:
onDidChangeCursor,onDidChangeContent
区别在于:
- 回调方式里,你到处处理原始
KeyboardEvent/MouseEvent,业务代码被浏览器细节污染。
- 现在,领域层拿到的是**“编辑器语义事件”**,离开浏览器也能复用:
比如以后做一个 canvas 版本、node+terminal 版本,都可以沿用大部分逻辑。
二、生命周期 & 内存管理上的优势
3. 统一的 IDisposable 模式
- 之前:
很容易忘,而且 listener 如果是匿名函数就更麻烦。
- 现在:
好处:
- 所有事件订阅(不管 DOM 还是内部事件)都变成“资源”,
只需在一个
DisposableStore / disposables[] 里统一清理。- 非常适合像 VS Code 这种有大量子组件 / 子视图的复杂 UI,防泄漏、防悬挂监听。
三、可组合性 & 解耦上的优势
4. 事件可以组合 / 派生
因为有统一的
Event<T> 抽象,你可以很自然地写:(示意,将来你加 map/filter/debounce 之后)
在回调模式下,你只能一个个写:
现在:
- 一个事件源,可以有 N 条“派生事件”,不同模块订阅不同视图。
- 模块之间通过 Event 对接,而不是直接互相调用方法,耦合度更低。
5. Model / View / Input 完全解耦
你现在已经做到:
- DOM 层(Input):
DomEmitter<KeyboardEvent/MouseEvent/UIEvent>- 适配层:
StandardKeyboardEvent / StandardMouseEvent- 模型层:
TextModel + onDidChangeContent- 视图层:
编辑器内部监听
onDidChangeContent → renderLines()监听
onDidChangeCursor → updateCaretPosition()跟之前直接在
keydown 回调里做:比起来,现在的好处是:
- 任何一个层级都可以换实现:
- 换一种 TextModel(piece table),视图逻辑基本不用改
- 改渲染方式(canvas / webgl),只要照样订阅事件
- 你现在的 Editor 其实就是一个小型的事件总线 + 响应式系统。
四、测试性 / 维护性上的优势
6. 更容易写单元测试
有
Event<T> 和 Emitter<T> 之后:- 你可以不依赖 DOM,直接 new 一个
TextModel,订阅onDidChangeContent,断言改动。
- 可以 new 一个 Editor,单独喂
StandardKeyboardEvent/StandardMouseEvent,
看它如何更新
cursor / selection,而不必真的触发浏览器事件。回调 + DOM 模式下,基本只能用 E2E / 浏览器驱动测试,
而事件系统让你可以在 Node 环境下跑一部分逻辑测试(VS Code 就是这么干的)。
7. 可观测性更强
VS Code 里很多功能是“监听一堆事件拼起来”的,比如:
- Auto-save:监听内容变化事件、失去焦点事件、窗口状态事件…
- Breadcrumb / minimap / outline:监听 model / cursor / selection / layout 更新事件…
用事件系统可以很自然地“订阅 + 聚合”,
而回调模式下,逻辑散落在不同 DOM 监听器里,维护成本更高。
五、跟“之前的回调写法”对比总结
维度 | 之前:直接 addEventListener / 回调 | 现在:Event + Emitter + DomEmitter |
抽象 | 每种事件自己约定签名 | 统一 Event<T> 抽象 |
类型安全 | 松散,容易 any | Event<Position> / Event<ModelChange> |
生命周期管理 | 手动 removeListener,易泄漏 | 统一 IDisposable + disposables |
组合能力 | 几乎没有,回调堆在一起 | 可做 map/filter/once/debounce 等 |
平台适配 | 到处写 `ctrlKey | ㅤ |
模块解耦 | 回调里混业务/渲染/状态 | Model / View / Input 各自监听事件 |
可测试性 | 靠真实 DOM 事件驱动 | 可在无 DOM 环境下测试核心逻辑 |
规模扩展 | 几十个监听就很乱 | 上百事件源也能维持结构清晰(VS Code 级别) |
如果你后面想继续贴近 VS Code,我们可以在这套架构上再往前走几步,比如:
- 给
Event加工具函数:once / map / filter / debounce
- 做
DisposableStore,帮你管理一堆IDisposable
- 设计
onDidChangeSelection,用刚搭好的鼠标/键盘事件来驱动选区系统
你要是愿意,我可以下一步直接给你一套
Event 工具函数 + 一个简单的 DisposableStore,顺手在 editor 里把它也用起来。