Rolf B: Eventhandling und Microtasks

Hallo alle,

ich bin mal wieder am Stöbern und habe eine Unklarheit.

Die Idee der JS Eventloop ist ja: Ein Task, dann die Microtasks, dann Layout/Rendering, dann wieder von vorn.

Jetzt stelle ich mir eine größere Webapplication vor, die abstrakt so aussehen könnte.

<body>
   <div>
      <button>Click</button>
   </div>
</body>

Wenn viel Script im Spiel ist, dann ist es durchaus denkbar, dass auf dem Body ein capture-Eventhandler für click existiert UND ein bubble-Eventhandler. Und auf dem div existiert ebenfalls ein capture-Eventhandler UND ein bubble-Eventhandler. Und auf dem button ist auch noch einer registriert. Es geht nicht drum, ob das sinnvoll ist, es soll nur einen komplexen Maximalfall konstruieren.

Klickt nun jemand den Button, wird das click-Event behandelt. Es wird ein Event-Path für den Button errichtet, entlang dieses Eventpath werden die capturing-Handler aufgerufen, dann der Target-Handler, dann in umgekehrter Reihenfolge die bubble-Handler.

Jeder dieser Handler KÖNNTE Microtasks auslösen und in die Microtask-Queue stellen.

Meine Frage ist nun, wie die Ausführungsreihenfolge ist, und ob das spezifziert ist. Ich habe im Moment nur ein müdes Tablet zur Hand, auf dem Weblabore schlecht laufen und wo es keine Konsole gibt, deshalb kann ich schlecht ausprobieren.

const div = document.querySelector("div"),
      btn = document.querySelector("button");
document.body.addEventListener("click", bodyClickCapture, true);
document.body.addEventListener("click", bodyClickBubble);
div.addEventListener("click", divClickCapture, true);
div.addEventListener("click", divClickBubble);
btn.addEventListener("click", btnClick);

Was soweit klar ist, ist diese Reihenfolge (sofern keiner der Handler in den Mechanismus eingreift, natürlich):

  1. bodyClickCapture - queueMicrotask(f1)
  2. divClickCapture - queueMicrotask(f2)
  3. btnClick - queueMicrotask(f3)
  4. divClickBubble - queueMicrotask(f4)
  5. bodyClickBubble - queueMicrotask(f5)

Was mir nicht klar ist: Ist aus Sicht der Engine jeder dieser Aufrufe ein neuer Task mit einer neuen Layoutphase? Oder laufen sie alle 5 und ERST DANN folgt die Layoutphase?

Was ist, wenn jede dieser 5 Funktionen mit queueMicrotask eine Funktion f1 bis f5 in die Microtask-Queue stellt?

Nach einigem Schmökern in der DOM Spec war meine Erwartung, dass die komplette click-Behandlung ein Task ist. Demnach sollten f1 bis f5 nach bodyClickBubble drankommen. Aber, Versuch macht kluch, und ich habe einen kleinen Versuch machen können, bevor mir jsFiddle auf dem Tablet erst den Run-Button versteckt und dann meinen Code gelöscht hat und ich die Lust verlor... da sah es so aus, als ob f3 nach btnClick laufen würde. Das war Chrome für Android.

Gibt es hier ein deterministisches Verhalten und wo ist es spezifiziert, weiß das jemand?

Rolf

--
sumpsi - posui - obstruxi
  1. Hallo Rolf,

    ich weiss nicht, ob du dein Problem schon lösen konntest. Aber vielleicht hilft dir das hier weiter: microtask and event listeners (ab 30:00 min)

    Etwas weiter vorne im Video (ab 27:00 min) wird auch gut erklärt, wie microtasks das rendering in the event loop beeinflussen.

    So die Kurzantwort: Es ist tricky und du musst genau aufpassen, wie der event ausgeführt wird 😉

    Gruß Michael

    1. Hallo Michael,

      ja, der Talk von Jake Archibald ist gut, aber er geht glaube ich an der Fragestellung von Rolf etwas vorbei, beziehungsweise erklärt nur einen Teil.

      Nach meinem Verständnis ist es wiefolgt:

      Wenn der Button in Rolfs Beispiel geklickt wird, bekommt der Browser vom Betriebssystem eine Nachricht und fügt daraufhin in der entsprechenden Taskqueue eine neue Task hinzu. (Hier ist zu beachten, dass es mehrere Taskqueues für verschiedene Arten von Tasks gibt, mit verschiedenen Prioritäten. Benutzerinteraktion hat typischerweise eine höhere Priorität als etwa Netzwerktasks, um die Seite responsiv zu halten.) Wenn dann alle früheren oder priorisierten Tasks und alle Microtasks abgearbeitet sind, kommt die Task für unser Event an die Reihe.

      Der Algorithmus der nun abgearbeitet wird ist in Firing Events beschrieben. Wir erzeugen ein Eventobjekt und folgen dann Dispatching Events, also wie von Rolf beschrieben einmal runter im Baum zum Zielobjekt und dann wieder rauf. Für jedes Objekt auf dem Ereignispfad für das wir passende Eventlistener registriert haben, rufen wir die hinterlegten Funktionen auf. Das ist in der Spec der Algorithmus Invoke. Hier müssen wir der Spur der Brotkrumen folgen. Der für uns relevante Abschnitt ist call a user object's operation. Das führt unseren Handler aus.

      Entscheidend für die Fragestellung ist dabei der letzte Schritt, das anschließende Aufräumen. Im Schritt clean up after running script heißt es nämlich:

      „If the JavaScript execution context stack is now empty, perform a microtask checkpoint.“

      Wenn einer unserer registrierten Eventhandler abgearbeitet wurde, ist der Callstack wieder leer, und an der Stelle ist ein Microtask Checkpoint vorgesehen. Wenn wir also wie im Beispiel von Rolf in den Eventhandlern Code ausführen, der Microtasks hinzufügt, dann werden die direkt nach der Rückgabe des jeweiligen Handlers ausgeführt, und bevor der nächste Handler aufgerufen wird. Das ist auch konsistent mit dem Beispiel aus Archibalds Talk.

      Wenn wir aus dem Programmcode heraus ein Event starten, statt als Antwort auf eine Benutzeraktion, also zum Beispiel click auf einem Elementobjekt aufrufen, dann sieht das natürlich anders aus. In dem Fall existiert ja nach der Rückgabe der Handler noch ein Kontext auf dem Callstack, nämlich der, von dem aus wir die Methode aufgerufen haben. Entsprechend werden hier die Microtasks erst nach allen Eventhandlern und dem aufrufenden Code abgearbeitet.

      Meines Wissens wird für die meisten Ereignisse eine Task hinzugefügt. Die kümmert sich dann wie oben beschrieben um die Ausführung der Handler, wobei eben zwischen jedem Aufruf Microtasks ausgeführt werden können. Die Layoutphase kommt danach.

      Was oft untergeht und für Verwirrung sorgt, ist, dass der Eventloop letztlich Browsercode ist und sich nicht nur um JavaScript dreht. Viele Tasks haben mit JavaScript gar nichts zu tun. Folglich gibt es da auch nicht zwingend eine eins-zu-eins Beziehung zwischen Tasks und Callbacks. Eine Task kann auch mehrere Callbacks ausführen, wobei aufgrund der obigen Regel nach jeder Ausführung die hinterlegten Microtasks abgearbeitet werden.

      Viele Grüße,

      Matthias

      1. Hallo Matthias,

        ich hätte jetzt vermutet, dass man mit dem Video die Fragen von Rolf beantworten kann:

        Was mir nicht klar ist: Ist aus Sicht der Engine jeder dieser Aufrufe ein neuer Task mit einer neuen Layoutphase? Oder laufen sie alle 5 und ERST DANN folgt die Layoutphase?

        Kommt drauf an, wie das Click-Event getriggert wird.

        Bei Click durch Nutzerinteraktion ist ein EventListener gleich einem Task mit dem Ergebnis:

        erster Eventlistener (= task) --> dann alle angesammelten Microtasks --> dann Rendering --> dann zweiter Eventlistener --> alle angesammelten Microtasks --> dann Rendering --> dritter Eventlistener --> alle angesammelten Microtasks --> dann Rendering usw.

        Bei Click via Javascript

        Zuerst alle EventListener --> erst dann alle angesammelten Microtasks --> und ganz zum Schluss erst ein rendering

        Was ist, wenn jede dieser 5 Funktionen mit queueMicrotask eine Funktion f1 bis f5 in die Microtask-Queue stellt?

        Siehe oben, wenn Click durch Nutzerinteraktion erfolgt, dann erfolgt rendering jeweils nach f1, f2, f3, f4, und f5. Bei Click durch Javascript erfolgt rendering erst, wenn f1 bis f5 abgearbeitet wurden.

        Gruss Michael

        1. Hallo Michael,

          worauf ich hinaus wollte, ist, dass man Task und Callback eben nicht pauschal gleichsetzen kann. Task ist in diesem Fall die Ereignisverarbeitung für ein bestimmtes Ereignis, nicht der Aufruf eines einzelnen Handlers für das Ereignis. Die Beschreibung "Zyklus: Task → Microtasks → Rendering" ist hier zu einfach. In der Praxis können innerhalb eines Eventloops an mehreren Stellen Microtasks ausgeführt werden, und auch der Rendering-Schritt wird nicht zwangsläufig in jeder Iteration tatsächlich ausgeführt.

          Wie in meinem Posting beschrieben, sieht die Spec vor, dass nach jedem Aufruf von Usercode, wenn der Callstack leer, ist Microtasks ausgeführt werden. Das impliziert aber nicht, dass eine Task nicht mehrere Funktionen ausführen kann. Die Task implementiert hier Dispatching Events und ruft im Zuge dessen die registrierten Eventhandler auf. Da der Callstack nach jedem Aufruf leer ist (wenn wir das Ereignis nicht programmatisch erzeugt haben), werden die hinterlegten Microtasks unmittelbar danach ausgeführt, vor dem Aufruf des nächsten Handlers. Das alles ist aber Teil der Verarbeitung ein und derselben Task.

          In Event Loop Processing Model sind die Schritte beschrieben, die in jedem Zyklus des Eventloops ausgeführt werden sollen. Der Rendering-Schritt ist dabei in update the rendering beschrieben. Neben all dem anderen Kram werden hier auch Callbacks die mit requestAnimationFrame hinterlegt wurden ausgeführt. Nehmen wir jetzt einmal an, wir registrieren in jedem von Rolfs Handlern nicht nur eine Microtask, sondern auch eine Animation Frame Callback. Wäre es nun so, dass jeder Handler selbst eine Task ist, dann müsste die Ausführungsreihenfolge so aussehen:

          captureBodyHandler
          captureBodyMicrotask
          captureBodyAnimationFrame
          
          captureDivHandler
          captureDivMicrotask
          captureDivAnimationFrame
          
          targetButtonHandler
          targetButtonMicrotask
          targetButtonAnimationFrame
          
          bubbleDivHandler
          bubbleDivMicrotask
          bubbleDivAnimationFrame
          
          bubbleBodyHandler
          bubbleBodyMicrotask
          bubbleBodyAnimationFrame
          

          Tut sie aber nicht. Die Ausführungsreihenfolge ist:

          captureBodyHandler
          captureBodyMicrotask
          captureDivHandler
          captureDivMicrotask
          targetButtonHandler
          targetButtonMicrotask
          bubbleDivHandler
          bubbleDivMicrotask
          bubbleBodyHandler
          bubbleBodyMicrotask
          
          captureBodyAnimationFrame
          captureDivAnimationFrame
          targetButtonAnimationFrame
          bubbleDivAnimationFrame
          bubbleBodyAnimationFrame
          

          Viele Grüße,

          Matthias