Rolf B: Frage zum Wiki-Artikel „requestAnimationFrame“

Beitrag lesen

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