最近在研究前端大型项目中要怎么管理满天飞的事件、模块间各种显示和隐式调用的问题,本文结合相应的源码分析,记录 VS Code 中的事件管理系统设计。
VS Code 事件
看源码的方式有很多种,带着疑问有目的性地看,会简单很多。
Q1: VS Code 中的事件管理代码在哪?
一般来说,说到事件,肯定是跟event
关键字相关,因此我们直接全局搜一下文件名(VS Code 下快捷键ctrl+p
):
一下子就出来了,这个路径是base\common\event.ts
的肯定是比较关键的。打开一看,里面比较关键的有两个:Event和Emitter。
Q2: VS Code 中的事件都包括了哪些能力?
先来看看Event
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
|
export namespace Event {
export function once<T>(event: Event<T>): Event<T> {}
export function snapshot<T>(event: Event<T>): Event<T> {}
export function debounce<T>(event: Event<T>, merge: (last: T | undefined, event: T) => T, delay?: number, leading?: boolean, leakWarningThreshold?: number): Event<T>;
export function stopwatch<T>(event: Event<T>): Event<number> {}
export function latch<T>(event: Event<T>): Event<T> {}
export function buffer<T>(event: Event<T>, nextTick = false, _buffer: T[] = []): Event<T> {}
export interface IChainableEvent<T> { event: Event<T>; map<O>(fn: (i: T) => O): IChainableEvent<O>; forEach(fn: (i: T) => void): IChainableEvent<T>; filter(fn: (e: T) => boolean): IChainableEvent<T>; filter<R>(fn: (e: T | R) => e is R): IChainableEvent<R>; reduce<R>(merge: (last: R | undefined, event: T) => R, initial?: R): IChainableEvent<R>; latch(): IChainableEvent<T>; debounce(merge: (last: T | undefined, event: T) => T, delay?: number, leading?: boolean, leakWarningThreshold?: number): IChainableEvent<T>; debounce<R>(merge: (last: R | undefined, event: T) => R, delay?: number, leading?: boolean, leakWarningThreshold?: number): IChainableEvent<R>; on(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable; once(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable; } class ChainableEvent<T> implements IChainableEvent<T> {}
export function chain<T>(event: Event<T>): IChainableEvent<T> {}
export function fromDOMEventEmitter<T>(emitter: DOMEventEmitter, eventName: string, map: (...args: any[]) => T = id => id): Event<T> {}
export function fromPromise<T = any>(promise: Promise<T>): Event<undefined> {} }
|
我们能看到,Event
中主要是一些对事件的处理和某种类型事件的生成。其中,除了常见的once
和 DOM 事件等兼容,还提供了比较丰富的事件能力:
Q3: VS Code 中的事件的触发和监听是怎么实现的?
到这里,我们只看到了关于事件的一些功能(参考Event
),而事件的触发和监听又是怎么进行的呢?
我们可以继续来看Emitter
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| export interface EmitterOptions { onFirstListenerAdd?: Function; onFirstListenerDidAdd?: Function; onListenerDidAdd?: Function; onLastListenerRemove?: Function; leakWarningThreshold?: number; }
export class Emitter<T> { constructor(options?: EmitterOptions) {}
get event(): Event<T> { }
fire(event: T): void {}
dispose() {} }
|
可以看到,Emitter
以Event
为对象,以简洁的方式提供了事件的订阅、触发、清理等能力。
Q4: 项目中的事件是怎么管理的?
Emitter
似乎有些简单了,我们只能看到单个事件发射器的使用。那各个模块之间的事件订阅和触发又是怎么实现的呢?
我们来全局搜一下关键字Emitter
:
搜出来很多地方都有使用,我们来看一下第一个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class WindowManager { public static readonly INSTANCE = new WindowManager(); private readonly _onDidChangeZoomLevel = new Emitter<number>(); public readonly onDidChangeZoomLevel: Event<number> = this._onDidChangeZoomLevel.event;
public setZoomLevel(zoomLevel: number, isTrusted: boolean): void { if (this._zoomLevel === zoomLevel) { return; } this._zoomLevel = zoomLevel; this._onDidChangeZoomLevel.fire(this._zoomLevel); } }
|
显然,在 VS Code 里,事件的使用方式主要包括:
- 注册事件发射器
- 对外提供定义的事件
- 在特定时机向订阅者触发事件
那么,其他地方又是怎样订阅这么一个事件呢?在这个例子中,由于浏览器实例唯一,可以通过挂载全局对象的方式来提供使用:
1 2 3 4 5 6 7 8 9 10
| export function onDidChangeZoomLevel(callback: (zoomLevel: number) => void): IDisposable { return WindowManager.INSTANCE.onDidChangeZoomLevel(callback); }
import { onDidChangeZoomLevel } from 'vs/base/browser/browser'; let zoomListener = onDidChangeZoomLevel(() => { });
|
除此之外,我们也可以通过创建实例调用来直接监听相关事件:
1 2 3 4
| const instance = new WindowManager(opts); instance.onDidChangeZoomLevel(() => { });
|
Q5: 事件满天飞,不会导致性能问题吗?
习惯使用一些前端框架的小伙伴们肯定比较有经验,我们如果在某个组件里做了事件订阅这样的操作,当组件销毁的时候是需要取消事件订阅的。否则该订阅内容会在内存中一直存在,除了一些异常问题,还可能引起内存泄露。
那么,VS Code 里又是怎么处理这样的问题呢?
其实我们在全局搜Emitter
的时候,也能看到一些地方的使用方式是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| export class Scrollable extends Disposable { private _onScroll = this._register(new Emitter<ScrollEvent>()); public readonly onScroll: Event<ScrollEvent> = this._onScroll.event;
private _setState(newState: ScrollState): void { const oldState = this._state; if (oldState.equals(newState)) { return; } this._state = newState; this._onScroll.fire(this._state.createScrollEvent(oldState)); } }
|
这里使用了this._register(new Emitter<T>())
这样的方式注册事件发射器,我们能看到该方法继承自Disposable
。而Disposable
的实现也很简洁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| export abstract class Disposable implements IDisposable { private readonly _store = new DisposableStore();
constructor() { trackDisposable(this); }
public dispose(): void { markTracked(this);
this._store.dispose(); }
protected _register<T extends IDisposable>(t: T): T { if ((t as unknown as Disposable) === this) { throw new Error('Cannot register a disposable on itself!'); } return this._store.add(t); } }
|
也就是说,每个继承Disposable
类都会有管理事件发射器的相关方法,包括添加、销毁处理等。其实我们仔细看看,这个Disposable
并不只是服务于事件发射器,它适用于所有支持dispose()
方法的对象:
Dispose 模式主要用来资源管理,资源比如内存被对象占用,则会通过调用方法来释放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| export interface IDisposable { dispose(): void; } export class DisposableStore implements IDisposable { private _toDispose = new Set<IDisposable>(); private _isDisposed = false;
public dispose(): void { if (this._isDisposed) { return; }
markTracked(this); this._isDisposed = true; this.clear(); }
public clear(): void { this._toDispose.forEach(item => item.dispose()); this._toDispose.clear(); }
public add<T extends IDisposable>(t: T): T { markTracked(t); if (this._isDisposed) { } else { this._toDispose.add(t); } return t; } }
|
因此,我们可以看到,在 VS Code 中是这样管理事件的:
- 抹平 DOM 事件等差异,提供标准化的
Event
和Emitter
能力。
- 通过注册
Emitter
,并对外提供类似生命周期的方法onXxxxx
的方式,来进行事件的订阅和监听。
- 通过提供通用类
Disposable
,统一管理多个事件发射器(或其他资源)的注册、销毁。
Q6: 上面只销毁了事件触发器本身的资源,那对于订阅者来说,要怎么销毁订阅的 Listener 呢?
或许读到这里的时候,你依然有点懵。看上去 VS Code 的Emitter
和Event
似乎跟常见的实现方式很相似,只是使用的方式有点不一样而已,到底有什么特别的呢?
不知道大家注意到了没,在 VS Code 中,注册一个事件发射器、订阅某个事件,都是通过this._register()
这样的方式来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| export class Button extends Disposable { private _onDidClick = this._register(new Emitter<Event>()); get onDidClick(): BaseEvent<Event> { return this._onDidClick.event; } }
export class QuickInputController extends Disposable { private getUI() { const ok = new Button(okContainer); ok.label = localize('ok', "OK"); this._register(ok.onDidClick(e => { this.onDidAcceptEmitter.fire(); })); } }
|
也就是说当某个类被销毁时,会发生以下事情:
- 它所注册的事件发射器会被销毁,而事件发射器中的 Listener、队列等都会被清空。
- 它所订阅的一些事件会被销毁,订阅中的 Listener 同样会被移除。
至于订阅事件的 Listener 是如何被移除的,可参考以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| export class Emitter<T> { get event(): Event<T> { if (!this._event) { this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => { if (!this._listeners) { this._listeners = new LinkedList(); } const remove = this._listeners.push(!thisArgs ? listener : [listener, thisArgs]);
let result: IDisposable; result = { dispose: () => { result.dispose = Emitter._noop; if (!this._disposed) { remove(); } } }; if (disposables instanceof DisposableStore) { disposables.add(result); } else if (Array.isArray(disposables)) { disposables.push(result); }
return result; }; } return this._event; } }
|
到这里,VS Code 中事件相关的管理的设计也都呈现出来了,包括:
- 提供标准化的
Event
和Emitter
能力
- 通过注册
Emitter
,并对外提供类似生命周期的方法onXxxxx
的方式,来进行事件的订阅和监听
- 通过提供通用类
Disposable
,统一管理相关资源的注册和销毁
- 通过使用同样的方式
this._register()
注册事件和订阅事件,将事件相关资源的处理统一挂载到dispose()
方法中
结束语
VS Code 中除了事件的管理,Dispose 模式还体现在各种其他资源的管理,包括插件等。
当我们遇到一些问题不知道该怎么解决的时候,可以试着站到巨人的肩膀上,说不定可以看到更多。
查看Github有更多内容噢:https://github.com/godbasin
更欢迎来被删的前端游乐场边撸猫边学前端噢
如果你想要关注日常生活中的我,欢迎关注“牧羊的猪”公众号噢