前段時間碰到了一個 Keybinding 相關(guān)的問題,于是探究了一番,首先大家可能會有兩個問題:Monaco Editor 是啥?Keybinding 又是啥?
創(chuàng)新互聯(lián)公司是一家集網(wǎng)站建設(shè),青浦企業(yè)網(wǎng)站建設(shè),青浦品牌網(wǎng)站建設(shè),網(wǎng)站定制,青浦網(wǎng)站建設(shè)報價,網(wǎng)絡(luò)營銷,網(wǎng)絡(luò)優(yōu)化,青浦網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強企業(yè)競爭力。可充分滿足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時我們時刻保持專業(yè)、時尚、前沿,時刻以成就客戶成長自我,堅持不斷學(xué)習(xí)、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實用型網(wǎng)站。
本文主要是針對 Monaco Editor 的 Keybinding 機制進行介紹,由于源碼完整的邏輯比較龐雜,所以本文中的展示的源碼以及流程會有一定的簡化。
文中使用的代碼版本:
Monaco Editor:0.30.1
VS Code:1.62.1
這里使用 monaco-editor 創(chuàng)建了一個簡單的例子,后文會基于這個例子來進行介紹。
import React, { useRef, useEffect, useState } from "react";
import * as monaco from "monaco-editor";
import { codeText } from "./help";
const Editor = () => {
const domRef = useRef(null);
const [actionDispose, setActionDispose] = useState();
useEffect(() => {
const editorIns = monaco.editor.create(domRef.current!, {
value: codeText,
language: "typescript",
theme: "vs-dark",
});
const action = {
id: 'test',
label: 'test',
precondition: 'isChrome == true',
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyL],
run: () => {
window.alert('chrome: cmd + k');
},
};
setActionDispose(editorIns.addAction(action));
editorIns.focus();
return () => {
editorIns.dispose();
};
}, []);
const onClick = () => {
actionDispose?.dispose();
window.alert('已卸載');
};
return (
);
};
export default Editor;
根據(jù)上面的例子,Keybinding 機制的總體流程可以簡單的分為以下幾步:
回到上面例子中創(chuàng)建 editor 的代碼:
const editorIns = monaco.editor.create(domRef.current!, {
value: codeText,
language: "typescript",
theme: "vs-dark",
});
初始化過程如下:
創(chuàng)建 editor 之前會先初始化 services,通過實例化 DynamicStandaloneServices 類創(chuàng)建服務(wù):
let services = new DynamicStandaloneServices(domElement, override);
在 constructor 函數(shù)中會執(zhí)行以下代碼注冊 keybindingService:
let keybindingService = ensure(IKeybindingService, () =>
this._register(
new StandaloneKeybindingService(
contextKeyService,
commandService,
telemetryService,
notificationService,
logService,
domElement
)
)
);
其中 this._register 方法和 ensure 方法會分別將 StandaloneKeybindingServices 實例保存到 disposable 對象(用于卸載)和 this._serviceCollection 中(用于執(zhí)行過程查找keybinding)。
實例化 StandaloneKeybindingService,在 constructor 函數(shù)中添加 DOM 監(jiān)聽事件:
this._register(
dom.addDisposableListener(
domNode,
dom.EventType.KEY_DOWN,
(e: KeyboardEvent) => {
const keyEvent = new StandardKeyboardEvent(e);
const shouldPreventDefault = this._dispatch(
keyEvent,
keyEvent.target
);
if (shouldPreventDefault) {
keyEvent.preventDefault();
keyEvent.stopPropagation();
}
}
)
);
以上代碼中的 dom.addDisposableListener 方法,會通過 addEventListener 的方式,在 domNode 上添加一個 keydown 事件的監(jiān)聽函數(shù),并且返回一個 DomListener 的實例,該實例包含一個用于移除事件監(jiān)聽的 dispose 方法。然后通過 this._register 方法將 DomListener 的實例保存起來。
回到例子中的代碼:
const action = {
id: 'test',
label: 'test',
precondition: 'isChrome == true',
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyL],
run: () => {
window.alert('chrome: cmd + k');
},
};
setActionDispose(editorIns.addAction(action));
注冊過程如下:
當(dāng)通過 editorIns.addAction 來注冊 keybinding 時,會調(diào)用 StandaloneKeybindingServices 實例的 addDynamicKeybinding 方法來注冊 keybinding。
public addDynamicKeybinding(
commandId: string,
_keybinding: number,
handler: ICommandHandler,
when: ContextKeyExpression | undefined
): IDisposable {
const keybinding = createKeybinding(_keybinding, OS);
const toDispose = new DisposableStore();
if (keybinding) {
this._dynamicKeybindings.push({
keybinding: keybinding.parts,
command: commandId,
when: when,
weight1: 1000,
weight2: 0,
extensionId: null,
isBuiltinExtension: false,
});
toDispose.add(
toDisposable(() => {
for (let i = 0; i < this._dynamicKeybindings.length; i++) {
let kb = this._dynamicKeybindings[i];
if (kb.command === commandId) {
this._dynamicKeybindings.splice(i, 1);
this.updateResolver({
source: KeybindingSource.Default,
});
return;
}
}
})
);
}
toDispose.add(CommandsRegistry.registerCommand(commandId, handler));
this.updateResolver({ source: KeybindingSource.Default });
return toDispose;
}
會先根據(jù)傳入的 _keybinding 創(chuàng)建 keybinding 實例,然后連同 command、when 等其他信息存入_dynamicKeybindings 數(shù)組中,同時會注冊對應(yīng)的 command,當(dāng)后面觸發(fā) keybinding 時便執(zhí)行對應(yīng)的 command。返回的 toDispose 實例則用于取消對應(yīng)的 keybinding 和 command。
回到上面代碼中創(chuàng)建 keybinding 實例的地方,createKeybinding 方法會根據(jù)傳入的 _keybinding 數(shù)字和 OS 類型得到實例,大致結(jié)構(gòu)如下(已省略部分屬性):
{
parts: [
{
ctrlKey: boolean,
shiftKey: boolean,
altKey: boolean,
metaKey: boolean,
keyCode: KeyCode,
}
],
}
那么,是怎么通過一個 number 得到所有按鍵信息的呢?往下看↓↓↓
先看看一開始傳入的 keybinding 是什么:
const action = {
id: 'test',
label: 'test',
precondition: 'isChrome == true',
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyL],
run: () => {
window.alert('chrome: cmd + k');
},
};
傳入的 keybinding 就是上面代碼中的 keybindings 數(shù)組中的元素,monaco.KeyMod.CtrlCmd = 2048,monaco.KeyCode.KeyL = 42,對應(yīng)的數(shù)字是 monaco-editor 中定義的枚舉值,與真實的 keyCode 存在對應(yīng)關(guān)系。所以注冊時傳入的 keybinding 參數(shù)為: 2048 | 42 = 2090
先簡單了解下 JS 中的位運算(操作的是32位帶符號的二進制整數(shù),下面例子中只用8位簡單表示):
按位與(AND)&
對應(yīng)的位都為1則返回1,否則返回0
例如:
00001010 // 10
00000110 // 6
------
00000010 // 2
按位或(OR)|
對應(yīng)的位,只要有一個為1則返回1,否則返回0
00001010 // 10
00000110 // 6
-------
00001110 // 14
左移(Left shift)<<
將二進制數(shù)每一位向左移動指定位數(shù),左側(cè)移出的位舍棄,右側(cè)補0
00001010 // 10
------- // 10 << 2
00 // 40
右移 >>
將二進制數(shù)每位向右移動指定位數(shù),右側(cè)移出的位舍棄,左側(cè)用原來最左邊的數(shù)補齊
00001010 // 10
------- // 10 >> 2
00000010 // 2
無符號右移 >>>
將二進制數(shù)每位向右移動指定位數(shù),右側(cè)移出的位舍棄,左側(cè)補0
00001010 // 10
------- // 10 >> 2
00000010 // 2
接下來看下是怎么根據(jù)一個數(shù)字,創(chuàng)建出對應(yīng)的 keybinding 實例:
export function createKeybinding(keybinding: number, OS: OperatingSystem): Keybinding | null {
if (keybinding === 0) {
return null;
}
const firstPart = (keybinding & 0x0000FFFF) >>> 0;
// 處理分兩步的keybinding,例如:shift shift,若無第二部分,則chordPart = 0
const chordPart = (keybinding & 0xFFFF0000) >>> 16;
if (chordPart !== 0) {
return new ChordKeybinding([
createSimpleKeybinding(firstPart, OS),
createSimpleKeybinding(chordPart, OS)
]);
}
return new ChordKeybinding([createSimpleKeybinding(firstPart, OS)]);
}
看下 createSimpleKeybinding 方法做了什么
const enum BinaryKeybindingsMask {
CtrlCmd = (1 << 11) >>> 0, // 2048
Shift = (1 << 10) >>> 0, // 1024
Alt = (1 << 9) >>> 0, // 512
WinCtrl = (1 << 8) >>> 0, // 256
KeyCode = 0x000000FF // 255
}
export function createSimpleKeybinding(keybinding: number, OS: OperatingSystem): SimpleKeybinding {
const ctrlCmd = (keybinding & BinaryKeybindingsMask.CtrlCmd ? true : false);
const winCtrl = (keybinding & BinaryKeybindingsMask.WinCtrl ? true : false);
const ctrlKey = (OS === OperatingSystem.Macintosh ? winCtrl : ctrlCmd);
const shiftKey = (keybinding & BinaryKeybindingsMask.Shift ? true : false);
const altKey = (keybinding & BinaryKeybindingsMask.Alt ? true : false);
const metaKey = (OS === OperatingSystem.Macintosh ? ctrlCmd : winCtrl);
const keyCode = (keybinding & BinaryKeybindingsMask.KeyCode);
return new SimpleKeybinding(ctrlKey, shiftKey, altKey, metaKey, keyCode);
}
拿上面的例子:keybinding = monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyL,即 keybinding = 2048 | 42 = 2090,然后看上面代碼中的:
const ctrlCmd = (keybinding & BinaryKeybindingsMask.CtrlCmd ? true : false);
運算如下:
// 2090 -> keybinding
// 2048 -> CtrlCmd
----------- // &
// 2048 -> CtrlCmd
再看keyCode的運算:
const keyCode = (keybinding & BinaryKeybindingsMask.KeyCode)
// 2090 -> keybinding
0000 // 255 -> KeyCode
----------- // &
000000 // 42 -> KeyL
于是便得到了 ctrlKey,shiftKey,altKey,metaKey,keyCode 這些值,接下來便由這些值生成SimpleKeybinding實例,該實例包含了上面的這些按鍵信息以及一些操作方法。
至此,已經(jīng)完成了 keybinding 的注冊,將 keybinding 實例及相關(guān)信息存入了 StandaloneKeybindingService 實例的 _dynamicKeybindings 數(shù)組中,對應(yīng)的 command 也注冊到了 CommandsRegistry 中。
當(dāng)用戶在鍵盤上按下快捷鍵時,便會觸發(fā) keybinding 對應(yīng) command 的執(zhí)行,執(zhí)行過程如下:
回到 StandaloneKeybindingServices 初始化的時候,在 domNode 上綁定了 keydown 事件監(jiān)聽函數(shù):
(e: KeyboardEvent) => {
const keyEvent = new StandardKeyboardEvent(e);
const shouldPreventDefault = this._dispatch(keyEvent, keyEvent.target);
if (shouldPreventDefault) {
keyEvent.preventDefault();
keyEvent.stopPropagation();
}
};
當(dāng) keydown 事件觸發(fā)后,便會執(zhí)行這個監(jiān)聽函數(shù),首先會實例化一個 StandardKeyboardEvent 實例,該實例包含了一些按鍵信息和方法,大致結(jié)構(gòu)如下(已省略部分屬性):
{
target: HTMLElement,
ctrlKey: boolean,
shiftKey: boolean,
altKey: boolean,
metaKey: boolean,
keyCode: KeyCode,
}
其中 keyCode 是經(jīng)過處理后得到的,由原始鍵盤事件的 keyCode 轉(zhuǎn)換為 monoco-editor 中的 keyCode,轉(zhuǎn)換過程主要就是兼容一些不同的瀏覽器,并根據(jù)映射關(guān)系得到最終的 keyCode。準(zhǔn)換方法如下:
function extractKeyCode(e: KeyboardEvent): KeyCode {
if (e.charCode) {
// "keypress" events mostly
let char = String.fromCharCode(e.charCode).toUpperCase();
return KeyCodeUtils.fromString(char);
}
const keyCode = e.keyCode;
// browser quirks
if (keyCode === 3) {
return KeyCode.PauseBreak;
} else if (browser.isFirefox) {
if (keyCode === 59) {
return KeyCode.Semicolon;
} else if (keyCode === 107) {
return KeyCode.Equal;
} else if (keyCode === 109) {
return KeyCode.Minus;
} else if (platform.isMacintosh && keyCode === 224) {
return KeyCode.Meta;
}
} else if (browser.isWebKit) {
if (keyCode === 91) {
return KeyCode.Meta;
} else if (platform.isMacintosh && keyCode === 93) {
// the two meta keys in the Mac have different key codes (91 and 93)
return KeyCode.Meta;
} else if (!platform.isMacintosh && keyCode === 92) {
return KeyCode.Meta;
}
}
// cross browser keycodes:
return EVENT_KEY_CODE_MAP[keyCode] || KeyCode.Unknown;
}
得到了 keyEvent 實例對象后,便通過 this._dispatch(keyEvent, keyEvent.target) 執(zhí)行。
protected _dispatch(
e: IKeyboardEvent,
target: IContextKeyServiceTarget
): boolean {
return this._doDispatch(
this.resolveKeyboardEvent(e),
target,
/*isSingleModiferChord*/ false
);
}
直接調(diào)用了 this._doDispatch 方法,通過 this.resolveKeyboardEvent(e) 方法處理傳入的 keyEvent,得到一個包含了許多 keybinding 操作方法的實例。
接下來主要看下 _doDispatch 方法主要干了啥(以下僅展示了部分代碼):
private _doDispatch(
keybinding: ResolvedKeybinding,
target: IContextKeyServiceTarget,
isSingleModiferChord = false
): boolean {
const resolveResult = this._getResolver().resolve(
contextValue,
currentChord,
firstPart
);
if (resolveResult && resolveResult.commandId) {
if (typeof resolveResult.commandArgs === 'undefined') {
this._commandService
.executeCommand(resolveResult.commandId)
.then(undefined, (err) =>
this._notificationService.warn(err)
);
} else {
this._commandService
.executeCommand(
resolveResult.commandId,
resolveResult.commandArgs
)
.then(undefined, (err) =>
this._notificationService.warn(err)
);
}
}
}
主要是找到 keybinding 對應(yīng)的 command 并執(zhí)行,_getResolver 方法會拿到已注冊的 keybinding,然后通過 resolve 方法找到對應(yīng)的 keybinding 及 command 信息。而執(zhí)行 command 則會從 CommandsRegistry 中找到對應(yīng)已注冊的 command,然后執(zhí)行 command 的 handler 函數(shù)(即keybinding 的回調(diào)函數(shù))。
先看看一開始的例子中的代碼:
const onClick = () => {
actionDispose?.dispose();
window.alert('已卸載');
};
卸載過程如下:
回到剛開始注冊時:setActionDispose(editorIns.addAction(action)),addAction 方法會返回一個 disposable 對象,setActionDispose 將該對象保存了起來。通過調(diào)用該對象的 dispose 方法:actionDispose.dispose(),便可卸載該 action,對應(yīng)的 command 和 keybinding 便都會被卸載。
對 Monaco Editor 的 Keybinding 機制進行簡單描述,就是通過監(jiān)聽用戶的鍵盤輸入,找到對應(yīng)注冊的 keybinding 和 command,然后執(zhí)行對應(yīng)的回調(diào)函數(shù)。但仔細探究的話,每個過程都有很多處理邏輯,本文也只是對其做了一個大體的介紹,實際上還有許多相關(guān)的細節(jié)沒有講到,感興趣的同學(xué)可以探索探索。