[VSCode 源码阅读笔记] VSCode 的事件处理

以日志服务 BufferLogService 为例

Posted by Nicodechal on 2020-03-10

本文从 VSCode 的日志服务之一了解 VSCode 的事件分发和处理。阅读版本为官方当前最新代码的 master 分支。

AbstractLogService

我们已经知道,VSCode 启动后,会从 src/main.js 异步加载并运行 CodeMain 实例的 .main() 方法并开始启动第一个页面。但是,在 startup(args) 一开始执行时,会先创建一个 bufferLogService,该服务用于避免在 Windows 下出现的并发访问 LOG 文件的问题 ( issue 41218 ) ,解决方式是先把产生的 LOG 信息缓冲,直到确认自己是唯一运行的实例再产生日志。

BufferLogService 继承了抽象类 AbstractLogService,该类主要用于设置日志级别,同时,当日志级别发生变化时,发出 onDidChangeLogLevel 事件,通知监听该事件的对象。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export abstract class AbstractLogService extends Disposable {
// 日志等级,默认为 INFO
private level: LogLevel = DEFAULT_LOG_LEVEL;
// 创建 Emitter 和相应的 Event
private readonly _onDidChangeLogLevel: Emitter<LogLevel> = this._register(new Emitter<LogLevel>());
readonly onDidChangeLogLevel: Event<LogLevel> = this._onDidChangeLogLevel.event;

setLevel(level: LogLevel): void {
if (this.level !== level) {
this.level = level;
// 设置新的等级时,触发 onDidChangeLogLevel 事件 ( 调用 Emitter 的 fire 方法 )
this._onDidChangeLogLevel.fire(this.level);
}
}

getLevel(): LogLevel {
return this.level;
}
}

这里使用了 VSCode 中典型的事件处理模式:

1
2
3
4
class AbstractLogService {
private readonly _onDidChangeLogLevel: Emitter<LogLevel> = this._register(new Emitter<LogLevel>());
readonly onDidChangeLogLevel: Event<LogLevel> = this._onDidChangeLogLevel.event;
}

这里 _register 主要用于资源回收步骤,不多说明。VSCode 中事件处理主要分为两部分:

  1. 定义一个分发器 Emitter
  2. 调用 Emitter 的 getter event 得到向该 Emitter 添加监听器的方法 ( Event 类型 )。

添加监听器的方法类似下面这样:

1
2
3
4
// 监听器的参数 level 的类型和分发器的泛型一致
this.onDidChangeLogLevel(level: LogLevel => {
// set level things ...
})

触发事件使用分发器的 fire 方法:

1
2
// 参数的类型和分发器的泛型一致
this._onDidChangeLogLevel.fire(this.level);

Emitter

和前面的过程相对应,一个 Emitter 有两个功能:

  1. getter event:得到一个向 Emitter 添加 listener 的函数。
    getter 在第一次调用时会生成函数 _event,该函数是 Event 类型,其会将传入的监听器加入到监听器列表 _listeners,相关代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    get event(): Event<T> {
    if (!this._event) {
    this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {
    // ...
    const remove = this._listeners.push(!thisArgs ? listener : [listener, thisArgs]);

    // ...
    };
    }
    return this._event;
    }
  2. fire:触发挂载该 Emitter 上的监听器们。
    fire 则直接循环触发 _listeners 中的所有监听器:

    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
    fire(event: T): void {
    if (this._listeners) {
    if (!this._deliveryQueue) {
    this._deliveryQueue = new LinkedList();
    }
    // 先将监听器和参数打包
    for (let listener of this._listeners) {
    this._deliveryQueue.push([listener, event]);
    }
    // 逐一触发所有监听器
    while (this._deliveryQueue.size > 0) {
    const [listener, event] = this._deliveryQueue.shift()!;
    try {
    if (typeof listener === 'function') {
    // 直接是一个函数
    listener.call(undefined, event);
    } else {
    // 包含参数
    listener[0].call(listener[1], event);
    }
    } catch (e) {
    onUnexpectedError(e);
    }
    }
    }
    }

BufferLogService

BufferLogService 是具备缓冲的 LOG 服务,其首先提供了产生不同级别 LOG 的函数 tracedebuginfowarnerrorcritical。他们都是对私有函数 _log 的封装,该函数不会立即打印 LOG 信息,直到设置了内部对象 _logger,这也是这个 logger 的特点:

1
2
3
4
5
6
7
8
9
10
private _log(level: LogLevel, ...args: any[]): void {
if (this._logger) {
// 如果有 logger,输出 LOG
const fn = getLogFunction(this._logger, level);
fn.apply(this._logger, args);
} else if (this.getLevel() <= level) {
// 否则级别正确就先加入缓冲
this.buffer.push({ level, args });
}
}

除此之外,该服务提供一个 setter,用于设置用于输出 LOG 的 logger, 设置后,也会立即进行 LOG 操作,把之前缓冲的部分输出:

1
2
3
4
5
6
7
8
9
10
11
set logger(logger: ILogService) {
this._logger = logger;

for (const { level, args } of this.buffer) {
const fn = getLogFunction(logger, level);
// 输出 LOG
fn.apply(logger, args);
}
// 清空 buffer
this.buffer = [];
}

小结

本文介绍了 VSCode 启动中的一个服务 BufferLogService 的相关功能,并以此为线索总结了 VSCode 中的事件机制的大致模式之一。总的来说,VSCode 中会以下面方式进行一些事件处理:

  1. 定义一个分发器 Emitter
  2. 调用 Emitter 的 getter event 得到向该 Emitter 添加监听器的方法 ( Event 类型 )。
  3. 调用 Emitterfire 触发相关监听器。