Himmel Bert: Problem mit Javascript Single Thread, der Update von DOM blockiert

Hallo,

kämpfe gerade damit, wie Javascript die Abarbeitung eines Codeblocks angeht (auch wenn es bei näherer Betrachtung durchaus Sinn ergibt) bzw. dass DOM Änderungen sofort passieren, das neuerliche "Zeichnen" eines DOM Elements aber ans Ende der Queue geschoben wird...

Bin bei meinen Recherchen auf Folgendes gestoßen: Why is setTimeout(fn, 0) sometimes useful?

Wer das nicht alles durchlesen möchte, kumikoda hat das Problem dort anhand eines Beispiels im Kern schön auf den Punkt gebracht: FIDDLE

...was aber wenn ich dutzende derartige Prozesse habe - bietet Javascript da Möglichkeiten mit der Queue direkt zu arbeiten?

Oder ist das ein klassischer Fall für async/await?

Ich habe Promises/async/await bisher nur z.B. in Verbindung mit dem Einholen von Datenbankeinträgen, dem Einhölen von API-Daten, etc. ==> EXTERN einzuholenden Quellen kennengelernt, aber sind Promises/async/await auch für die Bearbeitung der Abläufe INNERHALB eines Codeblocks (quasi ohne "Fremdeinwirkung") gedacht?

Danke, Bert.

  1. Hallo Bert,

    ich weiß auch nicht, ob man, außer mit setTimeout oder requestAnimationFrame aus der JS-Abarbeitungsqueue ausbrechen kann.

    Ich verwende hierfür auch noch mal setTimeout, bei lange laufenden Funktionen nehme ich aber inzwischen den Worker.

    https://wiki.selfhtml.org/wiki/JavaScript/Web_Worker

    Gruß
    Jürgen

  2. Hallo Bert,

    JavaScript hat zwei Queues - die EventLoop (Makrotasks) und die Mikrotasks.

    Makrotasks kommen von DOM Events und setTimeout oder setInterval.

    Mikrotasks kommen von Promises, async/await oder queueMicrotask().

    Nachdem ein Makrotask verarbeitet wurde, wird Queue der aufgelaufenen Mikrotasks geleert. Danach kommt der nächste Makrotask.

    D.h. setInterval(..., 0) und Promise.resolve().then(...) erzeugen beide einen asynchronen Eintrag. Aber das Promise läuft schon los, bevor der Browser seinen Paintjob erledigen kann. Denn das ist ein Teil der Makrotask-Schleife.

    Wenn Du also anzeigen willst, dass dein Programm rechnet, musst Du setTimeout(..., 0) verwenden. Machst Du es mit Promise.resolve(), rennt dein Langläufer los bevor dein "Bitte warten" angezeigt wird.

    Unser Wiki hat dazu leider keinen Artikel. Ich könnte einen schreiben, aber das wäre dann eine Übersetzung andererer Internetquellen und es wäre sicher auch sehr viel Arbeit. Deutsche Texte findet man, wenn nach "mikrotask makrotask" sucht, erstaunlich wenig.

    Das hier scheint recht ausführlich. MDN hat natürlich auch was zu sagen, als Übersicht und mit Tiefgang. Aber nicht auf deutsch.

    Rolf

    --
    sumpsi - posui - obstruxi
    1. Ok, das ist ein echtes Problem :/

      ...gibt's dazu irgendwelche Best-Practices?

      Mir fällt da jetzt eigentlich nur mehr eine Methode ein, die mir doch abenteuerlich hacky und nicht sehr wohlgeformt erscheint, nämlich rekursives TimeOut in die Richtung:

      // Alle durchzuführenden Funktionen,
      // nach ihrer Reihenfolge geordnet:
      let Reihenfolge = [Func1, Func2, Func3, Func4, Func5, ...];
      
      let counter = 0;
      let Ablauf = setTimeout(function Durchlauf() {
      	if (counter < Reihenfolge.length) {
      		Reihenfolge[counter]();
      		counter++;
      		Ablauf = setTimeout(Durchlauf, /* 0? */);
      	}
      },/* 0? */);
      

      Kann das so überhaupt funktionieren / bzw. laufen hier alle Tasks überhaupt noch garantiert hintereinander (also inklusive DOM-Repaints!) ab, wenn ich die Zeitspanne auf 0 setze?

      Erm...HILFE

      1. Lieber Himmel Bert,

        Mir fällt da jetzt eigentlich nur mehr eine Methode ein, die mir doch abenteuerlich hacky und nicht sehr wohlgeformt erscheint, nämlich rekursives TimeOut in die Richtung:

        wozu rekursiv? Warum nicht iterativ?

        // Alle durchzuführenden Funktionen,
        // nach ihrer Reihenfolge geordnet:
        [Func1, Func2, Func3, Func4, Func5, ...].forEach(function (f) {
          setTimeout(f, 0);
        })
        

        Liebe Grüße

        Felix Riesterer

        1. Hallo Felix,

          wenn ich die Spec richtig deute, müsste das tatsächlich funktionieren - weil nach jedem Eintrag in der Makrotask-Queue ein Rendering stattzufinden hat. Cool, ich hätte gedacht er leert erst die Task-Queue und rendert dann.

          Rolf

          --
          sumpsi - posui - obstruxi
      2. Hallo Bert,

        nein, das ist nicht hacky, das ist genau richtig so. Aber du brauchst keine globalen Variablen.

        // Alle durchzuführenden Funktionen, 
        // nach ihrer Reihenfolge geordnet:
        setTimeout(MakrotaskScheduler,
                   0,
                   [Func1, Func2, Func3, Func4, Func5, ...]);
        
        function MakrotaskScheduler(actions) {
           let action = actions.shift();   // Undefined bei leerem Array
           if (action) action();
           if (actions.length > 0)
              setTimeout(MakrotaskScheduler, 0, actions);
        }
        

        Ich schreibe die Funktion bei sowas gern separat, dann ist der setTimeout-Aufruf übersichtlicher.

        Die Parameter 3ff von setTimeout werden an die aufgerufene Funktion weitergereicht. Die shift Methode nimmt das erste Element aus dem Array heraus. Auf diese Weise wird das Array immer eins kleiner und die Actions werden durchgeführt. Jede einzelne Aktion findet in einem Makrotask statt.

        Das sieht rekursiv aus, ist es aber nicht, weil setTimeout nicht den Aufruf durchführt. Es hinterlegt nur den Aufruf in der Makrotask-Queue, den Aufruf selbst führt die JavaScript Runtime auf einem leeren Stack durch.

        Ob setTimeout richtig ist oder requestAnimationFrame, hängt davon ab, was Du eigentlich tun willst. Glatte Animationen gehen präziser mit letzterem.

        Rolf

        --
        sumpsi - posui - obstruxi
        1. nein, das ist nicht hacky, das ist genau richtig so.

          Das kommt darauf an, was Himmel Bert genau damit vorhat.

          Wenn es um rechenintensive, lang andauernde Berechnungen geht, dann wäre es vermutlich sinnvoller die Aufgabe von einem Worker erledigen zu lassen. Mit dem setTimeout kann man die Berechnung zwar in kleine Stücke zerteilen, und so das UI einigermaßen responsiv halten, aber die Gesamtasuführungsdauer nimmt auch zu, durch die vielen kleinen Pausen.

          Außerdem wäre es noch eine Überlegung wert, ob man Teile der Berechnung nicht auch auf dem Server ausführen kann. Hier muss man natürlich die Latenz mit abwägen.

          Wenn es um Animationen geht, dann wäre requestAnimation-Frame die bessere Alternative. So lassen sich die einzelnen Animationsschritte mit der Framerate takten. Außerdem kann die Browserengine hier optimieren, bspw. wenn sich das Browsertab gerade im Hintergrund befindent.

          1. Hallo,

            Wenn es um rechenintensive, lang andauernde Berechnungen geht, dann wäre es vermutlich sinnvoller die Aufgabe von einem Worker erledigen zu lassen. …

            wobei man dann die Berechnungen evtl. auch auf mehrere Worker und somit auf mehrere CPU-Kerne verteilen kann.

            Gruß
            Jürgen