beatovich: Performance JS-Function

hallo

Der folgende Code wird bei jedem Keydown Event (auf body registriert) ausgeführt:

var bindings={
	"7:ArrowDown":"MOVE_SELECTED_LINES_DOWN",
	"7:ArrowUp":"MOVE_SELECTED_LINES_UP",
	"7:ArrowLeft":"OUTDENT_SELECTED_LINES",
	"7:ArrowRight":"INDENT_SELECTED_LINES",
	"1:Tab":"INSERT_TAB",
	"4:u":"SELECT_URL_BAR",
	"4: ":"SELECT_TEXTAREA",
	"4:h":"OPEN_HELP",
	"4:m":"OPEN_MENU_MORE",
	"4:-":"SELECT_MENU_SECTION",
	"4:p":"UPDATE_PREVIEW",
	"5:<":"INPUT_HTML_ELEMENT",
	"5:f":"OPEN_SEARCH_DIALOG",
	"5:d":"DUPLICATE_SELECTED_LINES",
	"5:e":"ENCODE_HTML",
};

var keyFunction={
	"ENCODE_HTML":function(){
		alert("ENCODE_HTML")
	},
};

function keyController(ev, code){
	//console.log(ev);
	code = 0
		+ (ev.target == ta ? 1 : 0)
		+ (ev.shiftKey ? 2 : 0)
		+ (ev.ctrlKey ? 4 : 0)
		+ (ev.altKey ? 8 : 0);
	code += ":" + ev.key;
	if( bindings[code] !== undefined ){
	   //console.log(code);
		ev.preventDefault();
		ev.stopPropagation();
		keyFunction[bindings[code]]();
	}
}
~~~

Die Funktion soll möglichst performant sein.

Hat jemand eine Verbesserungsidee?

-- 
<https://beat-stoecklin.ch/pub/index.html>

  1. Hallo beatovich,

    besser als mit einem Dictionary-Lookup wirst Du es wohl nicht hinbekommen.

    Was Du mit ev.target==ta prüfst, ist mir nicht klar. Da fehlt was im geposteten Code, würde ich vermuten.

    Frage könnte sein, ob die Wartungsfreundlichkeit hinreichend ist. Das String-Matching von den Bindings zu den Functions gefällt mir nicht so gut. Wie wäre es hiermit:

    var keyFunctions = {
    	"OPEN HELP 1": function(evt) {
    		alert("Help me if you can!")
    	},
    	encodeHtml: function(evt) {
    		alert("ENCODE_HTML")
    	}
    };
    var bindings={
      // ....
      "4:h": keyFunctions["OPEN HELP 1"],  // so für Handler die kein gültiger JS Name sind
    	"5:e": keyFunctions.encodeHtml,      // oder so
    };
    

    Dann musst Du nach Identifikation einer relevanten Taste nicht mehr in ein zweites Dictionary einsteigen, sondern hast die passende Function direkt am Wickel. Du könntest damit auch mehrere KeyHandler-Container unterstützen.

    Das setzt natürlich voraus, dass alle Key-Funktionen vor Aufbau der Bindings bereit stehen und dass Du nicht dynamisch einem KeyFunction-Namen eine andere Function zuweisen willst (was aber Spagetti-Deluxe wäre).

    Du könntest einen Hander auch mit call aufrufen und ihm das Event-Objekt oder das Event-Target als this übergeben. Oder als einfachen Parameter. Ggf. braucht ein Keyhandler Informationen aus dem Event.

    Die Frage ist auch, ob Dir dieses Konstrukt hinreicht. Wie legst Du fest, dass das Handling einer Tastenkombination nur an die Handlerfunktion Xyz gehen soll, wenn sich das Event-Target innerhalb eines bestimmten Containers befindet? Möglicherweise hat Ctrl+Q ja im Container A eine andere Semantik als im Container B. Oder möchtest Du das im Einzelfall behandeln und eine Verteilerfunktion im Binding eintragen? Wäre vermutlich die performanteste Lösung...

    Es könnte noch eine Überlegung sein, zweistufig zu agieren, d.h. zunächst mit ev.key einzusteigen und die Prüfung auf Modifier nur zu machen, wenn für die Taste etwas hinterlegt ist. Aber ich glaube nicht, dass das lohnt. Du wirst nicht schnell genug Tasten drücken können, um einen Computer mit dem von Dir angedachten Code ins Schwitzen zu bringen. Selbst ein kleines Atom-Öfchen oder Brombeerchen nicht.

    Eine andere Frage, die mir gerade einfällt, ist Modularisierung. Angenommen, du hast mehrere sauber gekapselte JS-Module, die auf der Seite laufen. Jedes interessiert sich für ein paar Tasten. D.h. eigentlich müsste jedes Modul den KeyController als Parameter bekommen und sich dort registrieren können. Eine statische, manuell gepflegte Tabelle mit genau einem Handler pro Keycode kann dann zu starr sein, abgesehen davon dass die Handler-Methoden eventuell gar nicht vom Modul publiziert werden.

    Rolf

    --
    sumpsi - posui - clusi
    1. hallo

      Was Du mit ev.target==ta prüfst, ist mir nicht klar.

      ta ist ein Textarea-Element. Das ist etwas Testcode, die realen Namen sind Expliziter.

      Dann musst Du nach Identifikation einer relevanten Taste nicht mehr in ein zweites Dictionary einsteigen, sondern hast die passende Function direkt am Wickel. Du könntest damit auch mehrere KeyHandler-Container unterstützen.

      Ich dachte dass ein Gewinn noch möglich ist vor der Abfrage des ersten Binding-Schlüssels.

      Die meisten Events haben ja keinen zugeordnete Schlüssel Beispiel ev.key == Shift | Controll | Alt oder Events ohne Zusatztaste.

      Deshalb interessirt mich die Optimierung, falls ein Binding-Schlüssel vorliegt, nicht so sehr.

      Das setzt natürlich voraus, dass alle Key-Funktionen vor Aufbau der Bindings bereit stehen und dass Du nicht dynamisch einem KeyFunction-Namen eine andere Function zuweisen willst (was aber Spagetti-Deluxe wäre).

      Gewiss, vor allem da Anwender die Bindings selber konfigurieren.

      Die Frage ist auch, ob Dir dieses Konstrukt hinreicht. Wie legst Du fest, dass das Handling einer Tastenkombination nur an die Handlerfunktion Xyz gehen soll, wenn sich das Event-Target innerhalb eines bestimmten Containers befindet?

      Nun ja, derzeit berücksichtige ich nur die Editor Textarea als ein Modifikator. Später könnte ein anderer Modifikator hinzukommen Dann wirds halt 1,2,4,8,16

      Möglicherweise hat Ctrl+Q ja im Container A eine andere Semantik als im Container B. Oder möchtest Du das im Einzelfall behandeln und eine Verteilerfunktion im Binding eintragen? Wäre vermutlich die performanteste Lösung...

      Je mehr Abweichende Funtionalität ich per Kontxt biete um so schwerer wird der Lernprozess. Das möchte ich eigentlich vermiden.

      Es gibt aber Funktionen, die wirklich nur in der Textarea Sinn machen. Da möchte ich nicht das Defaultverhalten entziehen, wenn der Kontext ausserhalb der Textarea liegt.

      Es könnte noch eine Überlegung sein, zweistufig zu agieren, d.h. zunächst mit ev.key einzusteigen und die Prüfung auf Modifier nur zu machen, wenn für die Taste etwas hinterlegt ist.

      Du bekommst immer einen ev.code! Manchmal heisst er halt shift oder controll.

      Aber ich glaube nicht, dass das lohnt. Du wirst nicht schnell genug Tasten drücken können, um einen Computer mit dem von Dir angedachten Code ins Schwitzen zu bringen. Selbst ein kleines Atom-Öfchen oder Brombeerchen nicht.

      Eine andere Frage, die mir gerade einfällt, ist Modularisierung. Angenommen, du hast mehrere sauber gekapselte JS-Module, die auf der Seite laufen.

      Dann darf der Listener eben nicht auf der Obermenge der Teil-Doms agieren.

      Da schau ich schon, dass der Listener nur auf dem Container-Element des Editors registriert wird.

  2. Hat jemand eine Verbesserungsidee?

    Wenn du ein Online-Beispiel bastelst, würde ich mal mit den Performance-Dev-Tools einen flüchtigen Blick spendieren.

    1. hallo

      Hat jemand eine Verbesserungsidee?

      Wenn du ein Online-Beispiel bastelst, würde ich mal mit den Performance-Dev-Tools einen flüchtigen Blick spendieren.

      Ich werde im Verlauf des nächsten Wochen den Editor online testen. Dann werde ich mich an dich erinnern.

      Da gibts dann noch andere Online-relevante Tests. Mein Editor erlaubt URI-encoded HTML als query-string, der direkt in einem sandbox iframe via srcdoc dargestellt wird (also eine Fiddle Funktion).

  3. Moin @@beatovich,

    Der folgende Code wird bei jedem Keydown Event (auf body registriert) ausgeführt:

    Wird der komplette Code bei jedem Event ausgeführt oder nur die Funktion? Ersteres scheint mir unnötig, da sich die Variablen bindings und keyFunction nicht ändern und die Funktion keyController auch nur einmal definiert werden muss.

    Ansonsten hätte ich folgende Ideen, die du mal ausprobieren kannst:

    • Die Berechnung der Variable code ist doch eigentlich ein Bitmuster, wobei das sogar ohne Bitschubsen geht:
    code = ev.target === ta + 2 * !!ev.shiftKey + 4 * !!ev.ctrlKey + 8 * ev.altKey;
    
    • Die flache Datenstruktur von bindings mit dem String-basierten Dictionary-Lookup in einen Baum mit den Event-Eigenschaften überführen:
    bindings[ev.target][ev.shiftKey][ev.ctrlKey][ev.altKey];
    
    // oder als Methodenaufruf, in dem die Attribute zugeordnet werden:
    bindings.call(ev);
    
    • Notation der Methoden in keyFunction nicht als Strings:
    const keyFunction = {
        ENCODE_HTML: function() { /* … */ },
        // …
    };
    
    • Notation der Methoden direkt in bindings.

    Viele Grüße
    Robert

    1. hallo

      Moin @@beatovich,

      Der folgende Code wird bei jedem Keydown Event (auf body registriert) ausgeführt:

      Wird der komplette Code bei jedem Event ausgeführt oder nur die Funktion?

      Nur die Funktion keyController wird bei onkeydown ausgeführt.

      Ansonsten hätte ich folgende Ideen, die du mal ausprobieren kannst:

      • Die Berechnung der Variable code ist doch eigentlich ein Bitmuster, wobei das sogar ohne Bitschubsen geht:
      code = ev.target === ta + 2 * !!ev.shiftKey + 4 * !!ev.ctrlKey + 8 * ev.altKey;
      

      ggg, das braucht aber 3mal nachdenken.

      Noch kürzer wäre

      code = ev.shiftKey + ev.ctrlKey + ev.altKey + ":" + ev.code;

      Wenn ich ein intermediäres Objekt aus dem Konfigurationsobjekt erstelle, muss ja nur js den Code verstehen.

    2. Hallo Robert,

      code = ev.target === ta + 2 * !!ev.shiftKey + 4 * !!ev.ctrlKey + 8 * ev.altKey;

      Meinst Du, das ist tatsächlich signifikant schneller als

      code = (ev.target === ta ? 1 : 0) + (ev.shiftKey ? 2 : 0) + (ev.ctrlKey ? 4 : 0) + (ev.altKey ? 8 : 0)?

      bindings[ev.target][ev.shiftKey][ev.ctrlKey][ev.altKey];

      Ich denke, du meinst hier an erster Stelle den Keyname und nicht das Target, oder? ev.target ist ein DOM Objekt, das kann man nicht als Property-Key verwenden. Ansonsten hatte ich mir das auch schon überlegt; aber damit das funktioniert musst Du für jeden key drei Arraydimensionen erzeugen um nicht beim Auslesen versehentlich ins Nichts zu greifen und ein "cannot read property 'false' of undefined" zu bekommen.

      bindings = {
         "a": { false: { false: { false: null, true: null }, true: { false: HandleCtrlA, true: null },
                true:  { false: { false: null, true: null }, true: { false: null, true: null } }
      

      Finde ich jetzt schwer lesbar und wartbar, bzw. man müsste dann einen Helper schreiben der diese Struktur aus einer lesbaren Definition aufbaut.

      Oder Du musst Dich Schritt für Schritt durch die Kette hangeln und jedesmal prüfen, ob der Wert existiert. Oder einen try/catch drumherumlegen. Aber das ist beides nicht sonderlich fix. Die Prüfkette ist vermutlich noch fixer als ein Exception-Kontext.

      Die Code-Bitmap gefällt mir da noch am besten. Statt ihrer könnte man auch ein "Ctrl-", "Alt-" und "Shift-" vor den Keyname setzen und damit in die Bindings einsteigen, das verbessert die Lesbarkeit immens. Das Finden eines Property über seinen Namen sollte sehr schnell gehen, das wird in der JS-Runtime durch eine Hashtable unterstützt.

      Übrigens, beatovich, ArrowUp/-Down/-Left/-Right heißt Up/Down/Left/Right im Internet-Explorer!

      Rolf

      --
      sumpsi - posui - clusi
      1. hallo

        Übrigens, beatovich, ArrowUp/-Down/-Left/-Right heißt Up/Down/Left/Right im Internet-Explorer!

        Mich interessiert da bestenfalls Edge. Dieser Editor darf durchaus beanspruchen, dass sein Benutzer auch einen aktuellen Browser verwendet.

        Aber trotzdem Danke für den Hinweis.