15.062015

JavaScript debounce was ist das?

Falls du schon öfter JavaScript Code geschrieben hast und noch nie etwas
von "debounce" gehört hast stehen dir wohl ein paar Refactoring-Sessions bevor.

In JavaScript gibt es einige Events die vom Browser sehr häufig
getriggert werden können, dazu gehört z.B. das resize Event auf
dem Window Objekt. Während du mit der Maus ein Fenster
verkleinerst wird es unzählige male aufgerufen.

Wird im Callback des Eventhändlers z.B. ein HTML-Element neu positioniert
wird es eben auch unzählige male neu positioniert.
Das bedeutet wir haben einen ununterbrochenen reflow was ziemlich auf
die Perfomance der Seite drückt.

Beispiel:


function getScrollLeft() {
  return (window.pageXOffset !== undefined) ? window.pageXOffset :
    (document.documentElement || document.body.parentNode || document.body).scrollLeft;
}

function getScrollTop() {
  return (window.pageYOffset !== undefined) ? window.pageYOffset :
    (document.documentElement || document.body.parentNode || document.body).scrollTop;
}

function centreElement() {
    var element = document.querySelector('#centerMe');
    element.style.position = 'absolute';
    element.style.top = Math.max(0, ((document.body.offsetHeight - element.offsetHeight) / 2) + getScrollTop()) + 'px';
    element.style.left = Math.max(0, ((document.body.offsetWidth - element.offsetWidth) / 2) + getScrollLeft()) + 'px';
}

window.addEventListener('resize', function() {
  centreElement();
});

Der Trick ist es den callback erst auszuführen wenn nach einer bestimmten
Zeitspanne keine neuen Events mehr kommen. So haben wir nur EINE
Ausführung am Ende der Größenänderung.
Welche Zeitspanne wir warten müssen ist von Fall zu Fall unterschiedlich.

Immer wenn wir in JavaScript etwas nach einer bestimmten Zeit ausführen wollen
wird auf die setTimeout Funktion zurückgegriffen

Beispiel

...

var resizeTimeoutId = false;
window.addEventListener('resize', function() {
  clearTimeout(resizeTimeoutId);
  resizeTimeoutId = setTimeout(function() {
    centreElement();
  }, 200);
});

In diesem Beispiel wird das neu Positionieren erst nach
200 Millisekunden gestartet. Bekommen wir aber vorher ein
neues resize Event, brechen wir das ganze vorher mit
einem clearTimeout ab. Hierbei ist es wichtig die Timeout
ID Variable vor dem Evenet zu definieren um auch schon vorher
beim clear damit arbeiten zu können.

Das ganze ist jetzt ziemlich auf den speziellen Anwendungsfall
zugeschnitten und auch nicht besonders schön gelöst. Schöner geht
es mit einer allgemeinen Utility Funktion:


var debounce = function(func, wait) {
  var timeout;

  // die "verzögerte" Funktion
  return function() {

    var context = this, args = arguments;

    // Timeout resetten
    var later = function() {
    timeout = null;
      func.apply(context, args);
    };

    // Timer neu starten
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

Wenden wir nun unsere neue Utility Funktion mit unserem Beispiel:


...

window.addEventListener('resize', function() {
  debounce(centreElement, 200);
});

Fertig! Übersichtlicher Code und dazu auch noch perfomant.
Was will man mehr?

Viele Frameworks bieten schon eine eingebaut debounce
Funktion die man verwenden kann, ist das nicht der Fall
ist sie auch schnell selber geschrieben:

Es gibt noch perfomanter Möglichkeiten das gleiche Verhalten
zu Erzielen aber das Prinzip bleibt das gleiche. Eine
sehr effizient und gute Version findet auf dem "Modern JavaScript"
Blog von Rhys Brett-Bowen:
http://modernjavascript.blogspot.co.uk/2013/08/building-better-debounce.html

Der Quellcode in diesem Post hat noch nie einen Browser gesehen, für seine
Korrektheit kann keine Garantie gegeben werden ;)