代码路径为:
src/vs/base/common/event.ts
基础接口
和一般的 event 类工具定义事件为字符串不同,这里 Event
的定义为函数,:
interface Event<T> {
(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable;
}
- listener:事件订阅回调
- thisArgs: 指定回调中的 this
- disposables:用于统一管理订阅的回收
使用方不直接参与Event
对象的创建,而使用 Emitter
,其接口很简单:
class Emitter<T> {
constructor(options?: EmitterOptions) {
this._options = options;
}
dispose() {
// ..
}
/**
* For the public to allow to subscribe
* to events from this Emitter
*/
get event(): Event<T> {
// ...
}
/**
* To be kept private to fire an event to
* subscribers
*/
fire(event: T): void {
// ...
}
hasListeners(): boolean {
// ...
}
}
这里学到了两点:
1. Emitter<T>
比 eventemitter2 之类通过字符串作为事件名的工具类型更友好:
const emitter = new Emitter<{type: string; data: number}>();
emitter.event((e) => {
// e: {type: string; data: number}
});
emitter.fire({ type: 'add', data: 1 });
在项目中使用时,事件与 emitter 一一对应也更利于维护,而 eventemitter2
出现如下的写法是不可控的:
const emitter = new EventEmitter2();
emitter.emit('foo', 1);
emitter.emit('foo', { data: 1 });
emitter.on('foo', e => {
// what is e ?
})
- 事件有订阅就应该有取消订阅,不然一不小心就出现内存泄漏了,以 React 为例,一般这样写:
function TestComponent() {
useEffect(() => {
const emitter = new EventEmitter2();
const sub = e = > {
// do something with `e`
}
emitter.on('foo', sub);
return () => {
emitter.off('foo', sub);
}
}, []);
}
这么看好像没什么问题,但实际使用时容易忘记取消订阅,一个组件中事件比较多时又容易出现大把样板代码,而使用 VS Code 中的事件模块则要方便得多:
function TestComponent() {
// 新建一个 DisposableStore,用于收集所有的订阅
const disposableStore = useMemo(() => new DisposableStore(), []);
const emitter = useMemo(() => new Emitter<{type: string; data: number}>());
useEffect(() => {
emitter.event((e) => {
// do something with `e`;
}, null, disposableStore)
}, []);
useEffect(() => {
return () => {
// 取消所有订阅
disposableStore.clear();
}
}, [])
}
能力扩展
Emitter<T>
已经具备了事件模块最基本的能力,而通过 namespace Event
扩展的工具集则让事件模块变得更强大,先看一个例子:
const onEnterPress = Event.filter(
Event.map(
onKeyPress.event, e => new StandardKeyboardEvent(e)),
e => e.keyCode === KeyCode.Enter
);
再结合Event.chain
,可以写成这样:
const onEnterPress = Event.chain(onKeyPress.event)
.map(e => new StandardKeyboardEvent(e))
.filter(e => e.keyCode === KeyCode.Enter)
.event;
是不是有 RxJS 那味道了 🙂
源码解读
- onece
顾名思义,只会订阅一次事件
/**
* Given an event, returns another event which only fires once.
*/
function once<T>(event: Event<T>): Event<T> {
return (listener, thisArgs = null, disposables?) => {
// we need this, in case the event fires during the listener call
let didFire = false;
let result: IDisposable | undefined = undefined;
result = event(e => {
if (didFire) {
return;
} else if (result) {
result.dispose();
} else {
didFire = true;
}
return listener.call(thisArgs, e);
}, null, disposables);
if (didFire) {
result.dispose();
}
return result;
};
}
接受一个 Event
对象,并返回一个新的 Event
对象,其只会响应一次上游的事件触发。
实现的核心在于原事件与新事件的传递调用,逻辑还是挺清晰的,并不难理解。不过,自行实现类似的需求时需要特别注意,容易遗漏的情况来自 listener.call
内调用 once
。
- map
function map<I, O>(event: Event<I>, map: (i: I) => O, disposable?: DisposableStore): Event<O> {
return snapshot((listener, thisArgs = null, disposables?) => event(i => listener.call(thisArgs, map(i)), null, disposables), disposable);
}
通过传入的回调函数将给定的Event
映射成另一个Event
,在 snapshot
方法内,利用 Emitter
提供的钩子实现实现事件串联:
function snapshot<T>(event: Event<T>, disposable: DisposableStore | undefined): Event<T> {
let listener: IDisposable | undefined;
const options: EmitterOptions | undefined = {
onFirstListenerAdd() {
// 将新创建事件的 fire 作为传入事件的监听达到串联效果
listener = event(emitter.fire, emitter);
},
onLastListenerRemove() {
listener?.dispose();
}
};
if (!disposable) {
_addLeakageTraceLogic(options);
}
const emitter = new Emitter<T>(options);
disposable?.add(emitter);
return emitter.event;
}
filter
、forEach
也是类似 map
实现