Nico R.: Grundsätzliche Frage. Ist ein Callback hier der richtige Ansatz?

Nabend allerseits,

ich möchte in JS, nachdem eine Funktion ajaxdaten() und alle darin befindlichen Unterfunktionen abgearbeitet sind, eine Funktion button_einblenden() aufrufen. Sagen wir mal, die Funktion sieht vom Aufbau in etwa so aus. Der Code ist syntaktisch vereinfacht, es geht nur um den Ablauf:

function ajaxdaten() {
  function ajax1(sleep.php) {
    ...
    // sleep.php sendet nach 5 Sekunden einen Rückgabewert
    xhttp.onreadystatechange = function() {
      if (this.readyState == 4 && this.status == 200) {
        button_einblenden();
        // Rufe ich die Funktion hier auf, wird der Button wie gewünscht nach 5s eingeblendet
      }
    });
  }
  button_einblenden();
  // Rufe ich die Funktion hier auf, wird der Button sofort eingeblendet, die sleep.php läuft noch im Hintergrund
} 

Gibt es eine Möglichkeit, button_einblenden() erst dann aufzurufen, wenn alle Funktionen innerhalb von ajax_daten() abgearbeitet sind oder muss ich immer direkt an onreadystatechange() "horchen"? Ist ein callback das, was mich hier weiter bringen würde?

Jetzt, wo ichs schreibe, ahne ich, dass das vielleicht nicht geht, weil der AJAX-Aufruf ja asynchron abläuft (und auch soll)...

Nun hab ichs fertig getippt. Vielleicht kommt ja etwas dabei rum ;-)

Schöne Grüße Nico

  1. Hello Nico,

    Callbacks sind für die Lesbarkeit von Programmen mMn grundsätzlich Sch.....

    Besser gut vorausdenken, als mit AL (Artificial Laziness) zu arbeiten.

    In deinem Fall kommt die Frage hinzu, ob das Dokument, in dem die zusätzliche Aktion eine Wirkung haben soll, überhaupt noch existiert, wenn die Aktion ausgelöst werden würde!?

    Glück Auf
    Tom vom Berg

    --
    Es gibt soviel Sonne, nutzen wir sie.
    www.Solar-Harz.de
    S☼nnige Grüße aus dem Oberharz
    1. Hallo Tom,

      In deinem Fall kommt die Frage hinzu, ob das Dokument, in dem die zusätzliche Aktion eine Wirkung haben soll, überhaupt noch existiert, wenn die Aktion ausgelöst werden würde!?

      Ich bin nicht ganz sicher, ob ichs richtig verstehe. Aber ja, das Dokument enthält in erster Linie ein form-Element. Am Ende des Dokuments wird ein JS-Script aufgerufen, das u.a. die ajaxdaten() enthält, die in das bestehende Formular Daten einträgt und dann den submit-Button sichtbar bzw. aktiv schalten soll.

      Gruß Nico

  2. Hallo Nico R.,

    Ich unterstelle mal, dass dein sleep.php ein Dummy ist. Denn um 5s zu warten, braucht man keinen Servercode. Das geht mit setTimeout().,

    Beste Lösung: verwende fetch statt XMLHttpRequest und nutze die Promises, die dabei herauskommen.

    Zweitbeste Lösung: Bleibe bei XMLHttpRequest und initialisiere ein Promise, das Du im readyState 4 auf resolved setzt

    Drittbeste Lösung: Ein Callback.

    Code-Beispiele:

    Eine warte()-Funktion in JavaScript, mit setTimeout und Promises. Das Beispiel im verlinkten Artikel ist umfangreicher und enthält eine Erweiterung, mit der man den Wartezyklus abbrechen kann. Aber um nur 5s zu warten...

    function warte(millisecs) {
       return new Promise((resolve, reject) => {
          setTimeout(resolve, millisecs);
       });
    }
    
    warte(5000)
    .then(button_einblenden);
    

    Für Erklärungen schaue in den verlinkten Wiki-Artikel über Promises.

    Für einen Server-Request kann man fetch() verwenden (eine unserer vor sich hin rottenden Baustellen im Wiki 😕).

    fetch("sleep.php")
    .then(response => {
      if (response.ok)
        // Hier die richtige Empfangsmethode für die Antwortdaten verwenden!
        return response.text();   
      else
        throw new Error('Serveraufruf war nicht erfolgreich!');
    })
    .then(wert => {
       // tu was mit dem Wert
       button_einblenden();
    })
    .catch(error => {
       // Fehlerbehandlung, der throw von oben landet auch hier
    });
    

    Interessant an Promises ist, dass jeder .then ein neues Promise zurückgibt, das sich erfüllt, sobald der Code im .then erfolgreich ausgeführt wurde. Äh, ok, das ist nur die halbe Wahrheit, der then-Callback kann auch seinerseits ein Promise zurückgeben und das then-Promise erfüllt sich dann, wenn das zurückgegebene Promise erfüllt wird. Der erste .then nach fetch tut genau das, response.text() liefert ein Promise, das sich erfüllt, sobald der Text komplett über's Netz geladen ist.

    Durch diese Promise-Verkettung kann man eine Promise-Abfolge auch unterbrechen:

    function datenHolen() {
       return fetch("data.php")
       .then(response => response.text());
    }
    
    datenHolen()
    .then(text => /* tu was mit dem text */)
    

    Und dann gibt's noch die gezuckerte Version, mit await und async:

    async function datenHolen() {
       let response = await fetch("data.php");
       return await response.text();
    }
    
    // Folgender Code funktioniert nur in einer async-Funktion oder in
    // einem ECMAScript-Modul (<script type="module">):
    
    await datenHolen();
    button_einblenden();
    

    Gezuckert? await und async sind nichts weiter als ein callbackfreier Ersatz für die .then-Ketten von Promises, deswegen fällt das unter die Kategorie "Syntaxzucker".

    Rolf

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

      danke für deine ausführliche Antwort. Uuuund schon was gelernt... von fetch() hab ich noch nie was gehört. Das sieht ja wesentlich freundlicher aus als XMLHttpRequest. Ich bleibe jetzt aber mal bei zweiterem und stürze mich auf dein erstes Beispiel…

      Achso…

      Ich unterstelle mal, dass dein sleep.php ein Dummy ist. Denn um 5s zu warten, braucht man keinen Servercode. Das geht mit setTimeout().,

      Ja, damit konnte ich für mich eine Ladeverzögerung im AJAX-Script am einfachsten simulieren.

      Beste Lösung: verwende fetch statt XMLHttpRequest und nutze die Promises, die dabei herauskommen.

      Zweitbeste Lösung: Bleibe bei XMLHttpRequest und initialisiere ein Promise, das Du im readyState 4 auf resolved setzt

      function warte(millisecs) {
         return new Promise((resolve, reject) => {
            setTimeout(resolve, millisecs);
         });
      }
      
      warte(5000)
      .then(button_einblenden);
      

      Ich bin nicht sicher, ob ichs komplett verstanden habe, aber ich glaube, selbigen Effekt habe ich hiermit grundsätzlich auch erstmal, oder?

      function ajaxdaten() {
        function ajax1(sleep.php) {
          // sleep.php sendet nach 5 Sekunden einen Rückgabewert
          xhttp.onreadystatechange = function() {
            if (this.readyState == 4 && this.status == 200) {
              button_einblenden();
            }
          });
        }
      } 
      

      Ich versuche mein Problem mal nochmal etwas zu verdeutlichen. Ich eröffne in meiner Funktion ajaxdaten() einfach mal zwei AJAX-Verbindungen. Das ist nicht das, was bei mir passiert und auch nicht unbedingt logisch, aber vielleicht wird so mein Problem am klarsten:

      function ajaxdaten() {
        ajax1(sleep.php) {
          ...
          // sleep.php sendet nach 5 Sekunden einen Rückgabewert
          xhttp.onreadystatechange = function() {
            if (this.readyState == 4 && this.status == 200) {
              button_einblenden();
            }
          });
        ajax2(nosleep.php) {
          ...
          // sleep.php sendet ohne Verzögerung einen Rückgabewert
          xhttp.onreadystatechange = function() {
            if (this.readyState == 4 && this.status == 200) {
              button_einblenden();
            }
          });
        }
        button_einblenden();
      } 
      

      Im Grunde sieht man schon, dass es murx ist. Denn ajax2 und auch die Hauptfunktion dürfen ja den Button noch gar nicht einblenden, weil ajax1 noch nicht fertig ist. Um das abzufragen, müsste ich vielleicht mit einem Zähler die geöffneten Verbindungen hochzählen (toll, und schon hab ich wieder eine globale Variable außerhalb der Funktion) bzw. mit dem Aufruf der button_einblenden() herunterzählen und schauen, ob noch Verbindungen offen sind...

      Ließe sich das mit Promises klüger bewerkstelligen?

      Gruß Nico

      1. Hallo Nico R.,

        Ließe sich das mit Promises klüger bewerkstelligen?

        Ja. Promises haben concurrency-Methoden.

        let p1 = fetchText("res1").then(processText1);
        let p2 = fetchText("res2").then(processText2);
        
        Promise.allSettled([p1, p2])
        .then(button_einblenden);
        

        Auf fetchText gehe ich gleich ein. Sie liefert ein Promise, sobald der Text verfügbar ist.

        „Settled“ ist der Promise-Oberbegriff für resolved oder rejected. Die Promise.allSettled-Methode erzeugt ein neues Promise, das die an allSettled übergebenen Promises „bündelt“. Wenn alle Promises aus diesem Bündel resolved oder rejected wurden, resolved sich das Bündel-Promise. Der then-Handler bekommt ein Array mit allen Ergebnissen, für Details muss ich auf die MDN verweisen.

        Wenn Du in den Verarbeitungsfunktionen processText1 und processText2 fertig bist, werden p1 und p2 resolved, und der Button erscheint. Letztlich passiert da genau das, was Du Dir schon selbst überlegt hast - aber so ist das Rad fertig und braucht keinen neuen Erfinder.

        fetchText wäre mein Vorschlag für eine Funktion, die den Aufruf von fetch und den ersten .then-Handler bündelt, in dem Du den Erfolg der Operation abfragst.

        Vorhin habe ich das als kleine Pfeilfunktion notiert:

        .then(response => response.text())

        aber das ist natürlich nicht produktionsreif. Unser Wiki zeigt auf der Seite für fetch ein etwas besseres Errorhandling. Den HTTP Status abzufragen kann man sich im ok-Fall sparen, ok ist nur true wenn der Status im 2xx-Bereich liegt (-> MDN)

        Zum Beispiel so:

        async function fetchText(ressource) {
           let response = await fetch(ressource);
        
           if (response.ok)
              return response.text();
        
           throw new Error("Fehler beim Laden von " + ressource);
        }
        

        Wenn Du nicht Text, sondern JSON vom Server bekommst, dann verwendest Du natürlich response.json() statt response.text(), um gleich das dekodierte Objekt zu erhalten, und nennst die Funktion fetchObject oder so.

        Statt auf das allSettled-Promise mit then zu reagieren könnte man ebenfalls await verwenden, das hat aber die genannten Voraussetzungen (in async-Funktion notiert oder ein ECMAScript-Modul)

        Beachte, dass die then-Callbacks nie zur gleichen Zeit aufgerufen werden, wenn der then läuft. D.h. direkt hinter dem then hast Du das Ergebnis der Operation, die ein Promise liefert, keinesfalls zur Verfügung. Die then-Callbacks werden grundsätzlich in der sogenannten Mikrotask-Queue ausgeführt, und das heißt: nach dem Ende des "normalen" JavaScript-Codes und vor dem Beginn der Layout-Phase des Browsers. async und await kaschieren das, aber auch da ist es so, dass aller Code, der den Rückgabewert eines await verarbeitet, in der Mikrotask-Queue ausgeführt wird.

        Rolf

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

          das scheint in der Tat das zu sein, was ich suche. Die Beispiele auf der Mozilla-Seite sehen auf den ersten Blick hilfreich aus.

          Allerdings werde ich das wohl nochmal etwas zurückstellen müssen, da mir doch so einige Grundlagen in objektorientierter Programmierung fehlen und ich mich da tiefer einarbeiten müsste. Die Zeit bzw. Ruhe hab ich zur Zeit leider nicht. Aber wenns soweit ist, werd ich hier sicher nochmal nachhaken.

          Danke auf jeden Fall erstmal für deine Hinweise.

          Gruß Nico

          Hallo Nico R.,

          Ließe sich das mit Promises klüger bewerkstelligen?

          Ja. Promises haben concurrency-Methoden.

          let p1 = fetchText("res1").then(processText1);
          let p2 = fetchText("res2").then(processText2);
          
          Promise.allSettled([p1, p2])
          .then(button_einblenden);
          

          Auf fetchText gehe ich gleich ein. Sie liefert ein Promise, sobald der Text verfügbar ist.

          „Settled“ ist der Promise-Oberbegriff für resolved oder rejected. Die Promise.allSettled-Methode erzeugt ein neues Promise, das die an allSettled übergebenen Promises „bündelt“. Wenn alle Promises aus diesem Bündel resolved oder rejected wurden, resolved sich das Bündel-Promise. Der then-Handler bekommt ein Array mit allen Ergebnissen, für Details muss ich auf die MDN verweisen.

          Wenn Du in den Verarbeitungsfunktionen processText1 und processText2 fertig bist, werden p1 und p2 resolved, und der Button erscheint. Letztlich passiert da genau das, was Du Dir schon selbst überlegt hast - aber so ist das Rad fertig und braucht keinen neuen Erfinder.

          fetchText wäre mein Vorschlag für eine Funktion, die den Aufruf von fetch und den ersten .then-Handler bündelt, in dem Du den Erfolg der Operation abfragst.

          Vorhin habe ich das als kleine Pfeilfunktion notiert:

          .then(response => response.text())

          aber das ist natürlich nicht produktionsreif. Unser Wiki zeigt auf der Seite für fetch ein etwas besseres Errorhandling. Den HTTP Status abzufragen kann man sich im ok-Fall sparen, ok ist nur true wenn der Status im 2xx-Bereich liegt (-> MDN)

          Zum Beispiel so:

          async function fetchText(ressource) {
             let response = await fetch(ressource);
          
             if (response.ok)
                return response.text();
          
             throw new Error("Fehler beim Laden von " + ressource);
          }
          

          Wenn Du nicht Text, sondern JSON vom Server bekommst, dann verwendest Du natürlich response.json() statt response.text(), um gleich das dekodierte Objekt zu erhalten, und nennst die Funktion fetchObject oder so.

          Statt auf das allSettled-Promise mit then zu reagieren könnte man ebenfalls await verwenden, das hat aber die genannten Voraussetzungen (in async-Funktion notiert oder ein ECMAScript-Modul)

          Beachte, dass die then-Callbacks nie zur gleichen Zeit aufgerufen werden, wenn der then läuft. D.h. direkt hinter dem then hast Du das Ergebnis der Operation, die ein Promise liefert, keinesfalls zur Verfügung. Die then-Callbacks werden grundsätzlich in der sogenannten Mikrotask-Queue ausgeführt, und das heißt: nach dem Ende des "normalen" JavaScript-Codes und vor dem Beginn der Layout-Phase des Browsers. async und await kaschieren das, aber auch da ist es so, dass aller Code, der den Rückgabewert eines await verarbeitet, in der Mikrotask-Queue ausgeführt wird.

          Rolf