The most common in vscode is the command mode, whether it is a menu or a button click event is mostly executed in the form of a command, such a design mode is clearer in terms of code readability.
Earlier I wrote a demo about several common design patterns of js. If you are interested, read my article: https://blog.csdn.net/woyebuzhidao321/article/details/120235389 .
command mode
Let's look at the simplest command registration in vscode
src/vs/workbench/contrib/files/browser/fileActions.ts
CommandsRegistry.registerCommand({
id: NEW_FILE_COMMAND_ID,
handler: async (accessor) => {
await openExplorerAndCreate(accessor, false);
}
});
Here id
is unique and handler
is the corresponding execution function.
We jump into registerCommand
the method.
export const CommandsRegistry: ICommandRegistry = new class implements ICommandRegistry {
private readonly _commands = new Map<string, LinkedList<ICommand>>();
private readonly _onDidRegisterCommand = new Emitter<string>();
readonly onDidRegisterCommand: Event<string> = this._onDidRegisterCommand.event;
registerCommand(idOrCommand: string | ICommand, handler?: ICommandHandler): IDisposable {
if (!idOrCommand) {
throw new Error(`invalid command`);
}
if (typeof idOrCommand === 'string') {
if (!handler) {
throw new Error(`invalid command`);
}
return this.registerCommand({
id: idOrCommand, handler });
}
// add argument validation if rich command metadata is provided
if (idOrCommand.description) {
const constraints: Array<TypeConstraint | undefined> = [];
for (let arg of idOrCommand.description.args) {
constraints.push(arg.constraint);
}
const actualHandler = idOrCommand.handler;
idOrCommand.handler = function (accessor, ...args: any[]) {
validateConstraints(args, constraints);
return actualHandler(accessor, ...args);
};
}
// find a place to store the command
const {
id } = idOrCommand;
let commands = this._commands.get(id);
if (!commands) {
// 以健值对形式存储到map对象里统一管理
commands = new LinkedList<ICommand>();
this._commands.set(id, commands);
}
let removeFn = commands.unshift(idOrCommand);
let ret = toDisposable(() => {
removeFn();
const command = this._commands.get(id);
if (command?.isEmpty()) {
this._commands.delete(id);
}
});
// tell the world about this command
this._onDidRegisterCommand.fire(id);
return ret;
}
...
}
You can see that they are stored in the _commands map object in the form of key-value pairs for unified management. The id is used as the key, and the value is the linked list data structure
Next, let's see how to call
src/vs/workbench/contrib/files/browser/views/explorerView.ts through the button
registerAction2(class extends Action2 {
constructor() {
super({
id: 'workbench.files.action.createFileFromExplorer',
title: nls.localize('createNewFile', "New File"),
f1: false,
icon: Codicon.newFile,
precondition: ExplorerResourceNotReadonlyContext,
menu: {
id: MenuId.ViewTitle,
group: 'navigation',
when: ContextKeyExpr.equals('view', VIEW_ID),
order: 10
}
});
}
run(accessor: ServicesAccessor): void {
const commandService = accessor.get(ICommandService);
commandService.executeCommand(NEW_FILE_COMMAND_ID);
}
});
You can see that through the execution executeCommand
method to call, let's see what is implemented in the executeCommand method
src/vs/workbench/services/commands/common/commandService.ts
async executeCommand<T>(id: string, ...args: any[]): Promise<T> {
...
return this._tryExecuteCommand(id, args);
....
}
private _tryExecuteCommand(id: string, args: any[]): Promise<any> {
// 获取链表节点
const command = CommandsRegistry.getCommand(id);
if (!command) {
return Promise.reject(new Error(`command '${
id}' not found`));
}
try {
this._onWillExecuteCommand.fire({
commandId: id, args });
// 这里调用handler方法
const result = this._instantiationService.invokeFunction(command.handler, ...args);
this._onDidExecuteCommand.fire({
commandId: id, args });
return Promise.resolve(result);
} catch (err) {
return Promise.reject(err);
}
}
You can see that getCommand
the nodes of the linked list are obtained through the method through the id, let's look at the implementation of the invokeFunction function
src/vs/platform/instantiation/common/instantiationService.ts
invokeFunction<R, TS extends any[] = []>(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R {
let _trace = Trace.traceInvocation(fn);
let _done = false;
try {
const accessor: ServicesAccessor = {
get: <T>(id: ServiceIdentifier<T>) => {
if (_done) {
throw illegalState('service accessor is only valid during the invocation of its target method');
}
const result = this._getOrCreateServiceInstance(id, _trace);
if (!result) {
throw new Error(`[invokeFunction] unknown service '${
id}'`);
}
return result;
}
};
return fn(accessor, ...args);
} finally {
_done = true;
_trace.stop();
}
}
You can see that it actually calls the handler method. In this way, the process from registration to execution is completed.
publish-subscribe model
Define events in the class
static readonly _onHandleChangeSearchDetail = new Emitter<boolean>();
static onHandleChangeSearchDetail: Event<boolean> = SidebarPart._onHandleChangeSearchDetail.event;
event distribution
SidebarPart._onHandleChangeSearchDetail.fire(true);
Receive events in other classes
// 调用Emitter类的event方法,传入一个回调函数,fire时触发
this._register(SidebarPart.onHandleChangeSearchDetail(flag => {
console.log(333, flag);
}));
src/vs/base/common/event.ts
export class Emitter<T> {
private readonly _options?: EmitterOptions;
private readonly _leakageMon?: LeakageMonitor;
private readonly _perfMon?: EventProfiling;
private _disposed: boolean = false;
private _event?: Event<T>;
private _deliveryQueue?: LinkedList<[Listener<T>, T]>;
protected _listeners?: LinkedList<Listener<T>>;
constructor(options?: EmitterOptions) {
this._options = options;
this._leakageMon = _globalLeakWarningThreshold > 0 ? new LeakageMonitor(this._options && this._options.leakWarningThreshold) : undefined;
this._perfMon = this._options?._profName ? new EventProfiling(this._options._profName) : undefined;
}
...
/**
* For the public to allow to subscribe
* to events from this Emitter
*/
// 取.event的时候,执行的就是这里,它其实返回了一个方法:
get event(): Event<T> {
if (!this._event) {
this._event = (callback: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {
if (!this._listeners) {
this._listeners = new LinkedList();
}
const firstListener = this._listeners.isEmpty();
if (firstListener && this._options?.onFirstListenerAdd) {
this._options.onFirstListenerAdd(this);
}
let removeMonitor: Function | undefined;
let stack: Stacktrace | undefined;
if (this._leakageMon && this._listeners.size >= 30) {
// check and record this emitter for potential leakage
stack = Stacktrace.create();
removeMonitor = this._leakageMon.check(stack, this._listeners.size + 1);
}
if (_enableDisposeWithListenerWarning) {
stack = stack ?? Stacktrace.create();
}
const listener = new Listener(callback, thisArgs, stack);
const removeListener = this._listeners.push(listener);
if (firstListener && this._options?.onFirstListenerDidAdd) {
this._options.onFirstListenerDidAdd(this);
}
if (this._options?.onListenerDidAdd) {
this._options.onListenerDidAdd(this, callback, thisArgs);
}
const result = listener.subscription.set(() => {
if (removeMonitor) {
removeMonitor();
}
if (!this._disposed) {
removeListener();
if (this._options && this._options.onLastListenerRemove) {
const hasListeners = (this._listeners && !this._listeners.isEmpty());
if (!hasListeners) {
this._options.onLastListenerRemove(this);
}
}
}
});
if (disposables instanceof DisposableStore) {
disposables.add(result);
} else if (Array.isArray(disposables)) {
disposables.push(result);
}
return result;
};
}
return this._event;
}
/**
* To be kept private to fire an event to
* subscribers
*/
// 循环派发了所有注册的事件
fire(event: T): void {
if (this._listeners) {
// put all [listener,event]-pairs into delivery queue
// then emit all event. an inner/nested event might be
// the driver of this
if (!this._deliveryQueue) {
this._deliveryQueue = new LinkedList();
}
for (let listener of this._listeners) {
this._deliveryQueue.push([listener, event]);
}
// start/stop performance insight collection
this._perfMon?.start(this._deliveryQueue.size);
while (this._deliveryQueue.size > 0) {
const [listener, event] = this._deliveryQueue.shift()!;
try {
listener.invoke(event);
} catch (e) {
onUnexpectedError(e);
}
}
this._perfMon?.stop();
}
}
hasListeners(): boolean {
if (!this._listeners) {
return false;
}
return (!this._listeners.isEmpty());
}
}