编程随想 · 2022 年 06 月 29 日 0

VS Code 源码之:Event

代码路径为:

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 ?
})
  1. 事件有订阅就应该有取消订阅,不然一不小心就出现内存泄漏了,以 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 那味道了 🙂

源码解读

  1. 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

  1. 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;
}

filterforEach也是类似 map 实现