Thomas Mell: Frage zum Wiki-Artikel „requestAnimationFrame“

problematische Seite

Die Aussage "Sie können keine eigenen Parameter für diese Funktion bereitstellen." ist falsch.

requestAnimationFrame(function () { myFunction(para1, para2); }

oder

requestAnimationFrame(() => { myFunction(para1, para2); }

  1. problematische Seite

    Lieber Thomas,

    Die Aussage "Sie können keine eigenen Parameter für diese Funktion bereitstellen." ist falsch.

    ist sie das? Ich beweise Dir, dass sie nicht falsch ist.

    Auf der fraglichen Seite wird davon gesprochen, dass die Methode requestAnimationFrame des window-Objektes ein Funktionsobjekt als Argument benötigt. Dieses ist der einzige Parameter, den diese Methode auswertet, weitere Parameter können zwar an diese Methode übergeben werden, jedoch werden sie nicht ausgewertet werden. Und genau das beweist Du im Grunde auch mit Deinen eigenen beiden Code-Beispielen:

    requestAnimationFrame(function () { myFunction(para1, para2); }

    oder

    requestAnimationFrame(() => { myFunction(para1, para2); }

    In beiden Beispielen übergibst Du genau ein Argument an requestAnimationFrame, nämlich ein Funktionsobjekt in Form einer anonymen Funktion. Das entspricht zu 100% der Aussage, von der Du behauptest, sie sei aber falsch. Wenn sie falsch wäre, dann könnte man etwas in dieser Art tun:

    requestAnimationFrame(
      (timeStamp, x) => myFunction("noch was", x, timeStamp),
      42,
      "Unsinn"
    );
    

    In meinem Beispiel werden drei Argumente an requestAnimationFrame übergeben:

    1. ein Funktionsobjekt (anonyme Pfeilfunktion), welches zwei Parameter entgegen nimmt
    2. die Ganzzahl 42
    3. die Zeichenkette „Unsinn“

    Das zweite und dritte Argument wird von der Methode requestAnimationFrame ignoriert werden. Genau das ist der Hintergrund der von Dir als falsch bezeichneten Aussage. Die Methode requestAnimationFrame ist so spezifiziert, dass sie eben nur das Funktionsobjekt nimmt, und keine weiteren Argumente auswertet.

    Schauen wir uns einmal die Parameter des übergebenen Funktionsobjektes an. Die Variable timeStamp wird beim ersten Frame den Wert undefined erhalten, bei einem späteren Frame dagegen eine Ganzzahl, welche die Millisekunden abbilden wird, die seit dem Beginn der Animation vergangen sind. Da ein browserinterner Mechanismus unsere Funktion aufrufen wird, wird er steuern, wieviele Argumente er an unsere Funktion übergeben wird, und mit welchen Werten als Inhalt. Das bedeutet, dass der zweite Parameter x niemals etwas anderes als undefined sein wird, weil der Mechanismus hinter requestAnimationFrame so spezifiziert ist, dass er eben nur diesen einen Wert an unsere Funktion übergeben wird.

    Vermutlich sitzt Du einem Missverständnis auf: Deine beiden anonymen Funktionen nehmen beide überhaupt gar kein Argument entgegen, denn die runden Klammern sind bei beiden leer! Dass innerhalb Deiner anonymen Funktionen dann eine ganz andere Funktion namens myFunction aufgerufen wird, die dann auch noch zwei Parameter gefüttert bekommt, ist etwas völlig anderes und hat mit requestAnimationFrame selbst nichts mehr zu tun. Es ist in Deinen Beispielen auch ungeklärt, woher die Variablen para1 und para2 stammen, oder welche Werte sie haben sollen, und wie diese Werte da hinein gekommen sein sollen. Aber das ist wie schon gesagt völlig unabhängig davon, wie requestAnimationFrame selbst funktioniert.

    Die Aussage im Artikel ist also sachlich korrekt.

    Liebe Grüße

    Felix Riesterer

  2. problematische Seite

    Hallo Thomas,

    Felix hat mich überholt, ich schreib's aber trotzdem. Ey Felix, du bist krank, schon Dich 😉

    Du kannst der anonymen Funktion, die Du jetzt an requestAnimationFrame übergibst, weiterhin keine Parameter mitgeben. Du hast lediglich einen - potenziell fehleranfälligen und ressourcenhungrigen - Workaround gebaut. Und falsch noch dazu.

    Denn: durch deine anonyme Funktion erzeugst Du eine Closure, die den Kontext einschließt, der im Moment der Erzeugung der anonymen Funktion gilt. Ich erweitere deine Lösung mal etwas, damit man sieht, wo parm1 und parm2 herkommen könnten:

    function doAnimation() {
       let parm1 = document.querySelectorAll(".foo"),
           parm2 = 42;
    
       requestAnimationFrame(function() { myFunction(parm1, parm2); });
    
       parm2 = 19;
       requestAnimationFrame(function() { otherFunction(parm1, parm2); });
    
       parm2 = 91;
    }
    

    Ja, ich weiß, Bernhard Hoëcker und Wigald Boning wären begeistert. Ein Musterbeispiel von „Nicht nachmachen“.

    Ressourcenleck: Ich habe jetzt nur 2 Variablen verwendet, aber stell Dir vor, doAnimation hätte 20 davon. All diese Variablen würden Teil der Closure werden, die das Erzeugen der anonymen Funktion bildet, und diese Closure hängt dann zusammen mit der Funktion in der Animationswarteschlange. Zugegeben, da bleibt sie nicht lange, aber Daten, die länger leben als nötig, sind mit Vorsicht zu betrachten.

    Fehleranfällig: Der Animationszyklus läuft außerhalb der normalen JavaScript-Verarbeitung, d.h. wenn myFunction oder otherFunction aufgerufen wird, hat parm2 NICHT den Wert, den es beim Registrieren der Funktion hatte, sondern den Wert, der ihm zuletzt zugewiesen wurde. Also 91.

    Falsch: Die Callbackfunktion von requestAnimationFrame erhält einen timestamp-Parameter. Den muss dein Workaround unbedingt an myFunction durchreichen!


    Im Artikel habe ich beschrieben, dass sich die Callback-Funktion vor allem um das Interfacing mit requestAnimationFrame kümmern sollte. Also die vergangene Zeit berechnen, und vor allem einen neuen Animationframe bestellen. Sowas schreibt man dann nur einmal. Im Wiki habe ich das mit globalen Variablen gemacht, aber man könnte hier auch eine Klasse nutzen.

    class Animator {
       #callback;
       #renderer;
       #startZeit = undefined;
       #vorigeZeit;
       #requestId = undefined;
    
       constructor(renderer) {
          this.#callback = this.#animate.bind(this);
          this.#renderer = renderer;
       }
       run() {
          this.#requestId = requestAnimationFrame(this.#callback);
       }
       stop() {
          if (this.#requestId)
             cancelAnimationFrame(this.#requestId);
       }
       #animate(timestamp) {
          if (this.#startZeit === undefined) {
             this.#startZeit = timestamp;
             this.#vorigeZeit = timestamp;
          }
          this.#renderer(timestamp - this.#startZeit, timestamp - this.#vorigeZeit);
          this.#vorigeZeit = timestamp;
          this.run();
       }
    }
    
    const a = new Animator(myFunction);
    a.run();
    ...
    a.stop();
    

    Immer noch keine eigenen Parameter? Nein, immer noch nicht. Das ist etwas, worum sich der VERWENDER der Klasse kümmern kann, nicht die Klasse. Denn diese Parameter könnten ja auch etwas sein, was sich ändern können muss. Und dann wäre doch wieder ein Objekt ideal:

    const anim = {
       foo: document.getElementById("foo"),
       status: 12,
       animate(animTime, diffTime) {
          ...
          this.status = 99;
       }
    }
    const b = new Animator(anim.animate.bind(anim));
    

    Durch bind ist der Callback, den der Animator bekommt, nun an das anim-Objekt gebunden, d.h. wenn der Animator die render-Funktion ruft, findet sich anim.animate automatisch im Kontext von anim wieder und kann es als this benutzen. Und hat nun alle Daten, die es braucht und kann sich sogar darin etwas merken, wenn's not tut.

    Achso, ist alles ungetestet vom Hirn ins Forum gedonnert. Das ist aber was, was man mal im Wiki als Beispiel mit einbauen könnte. Wenn ich einmal Zeit hätt, dideldideldum.

    Rolf

    --
    sumpsi - posui - obstruxi