Aufs Wesentliche konzentrieren mit der Mini-Bibliothek

Underscore.js – ein kleines Framework mit Fokus
Keine Kommentare

In der Welt der JavaScript-Programmierung scheint es einen ungeschriebenen Leitsatz zu geben: Je aufdringlicher und umfangreicher ein Framework, desto besser muss es sein. Das gilt aber nicht immer: Underscore.js ist eine im Minified-Zustand nur 6 KB große Bibliothek, die darauf ausgelegt ist, Entwicklern eine Gruppe von gerne verwendeten Funktionen anzubieten und sich sonst aus Applikationsarchitektur und Co. herauszuhalten.

Aus dieser vergleichsweise offenen Aufgabendefinition von Underscore.js folgt, dass es nicht unbedingt einfach ist, einen Finger auf die Form von Underscore.js zu legen. Die Bibliothek wird von Jeremy Ashkenas entwickelt, der unter anderem durch CoffeeSkript bekannt ist. Underscore.js wird im angelsächsischen Raum gerne als Unterstützungswerkzeug für all jene bezeichnet, die bei der Arbeit mit JavaScript funktionale Programmierungsparadigmen umsetzen möchten, ohne von Hand komplexe Algorithmen zu implementieren. Die Beschreibung ist nicht unbedingt falsch, wie der Artikel anhand einiger interessanter Features zeigen wird.

Die folgenden Schritte werden auf einer mit Ubuntu 14.04 betriebenen Workstation durchgeführt. Wie bei fast allen JavaScript-Bibliotheken gilt auch hier, dass im ersten Schritt ein Testharnisch erforderlich ist:

<html>
<head>
<script src="underscore.js"></script>
</head>
</html>

Underscore.js mag in verschiedenen CDNs zum Download bereitliegen. Ob der geringen Dateigröße ist es meiner Meinung nach aber sinnvoller, den Code entweder auf dem lokalen Server zu belassen oder ihn per Inlining in die Datei zu kopieren: DNS-Requests nehmen auch Zeit in Anspruch.

Unsere Beispielprogramme nutzen die nicht modizierte Version des Codes. Sie ist zwar um den Faktor zehn größer, bietet aber diverse Fehlerüberprüfungen und andere Hilfsmittel an, die bei Experimenten mit den Bibliotheksfunktionen hilfreich sein können.

Eine Frage der Mengenlehre

Die Verarbeitung von größeren Informationsmengen ist seit jeher relevant: Schon in Oldies wie Pascal oder C fand sich eine Unterstützung für Arrays. Die Underscore. js-Bibliothek verteilt einen Gutteil der in ihr enthaltenen Funktionalität auf Methoden, die für das Hantieren mit Arrays vorgesehen sind.

Im Interesse der Vereinfachung arbeiten wir in den folgenden Schritten mit einem globalen Feld aus Zahlen, das als window.myArray = [22,33,12,43,12,22,14,15]; definiert ist. Die Zahlen entstammen einem Zufallsgenerator und haben keinerlei weitere Bedeutung. Für eine erste Fingerübung wollen wir auf folgenden Code zurückgreifen:

<body>
<script type="text/javascript">
window.myArray = [22,33,12,43,12,22,14,15];
_.each(window.myArray,alert);
</script>
</body>

Underscore.js nistet sich in einem Objekt namens _ ein. Achten Sie darauf, dass das Objekt eine Gruppe von Methoden darstellt: Die Sequenz aus einem Unterstrich und einem Punkt wirkt auf Personen, die in anderen Programmiersprachen aufgewachsen sind, mitunter befremdlich.

Unser erstes Beispiel nutzt die each-Funktion (Kasten: „Mit Namen“). Sie verhält sich im Großen und Ganzen so, wie man es vom Namen her erwarten würde: Die als zweiter Parameter übergebene Iteratorfunktion wird für jedes Element des übergebenen Felds einmal aktiviert. Das können Sie in einem Browser Ihrer Wahl überprüfen. Mehr über die Rolle des zweiten Parameters kann man auf der  underscore.js-Webseite erfahren. Das Underscore-Team bietet dort einen in HTML gehaltenen Komplettabdruck des Bibliothekscodes an, der zudem mit diversen Annotationen ausgestattet ist (Abb. 1).

Abb. 1: Detailverliebte Dokumentation ist für Projekte von Jeremy Ashkenas typisch

Abb. 1: Detailverliebte Dokumentation ist für Projekte von Jeremy Ashkenas typisch

Wer bis zur Definition der each-Funktion scrollt oder nach ihr sucht, findet die in Listing 1 sichtbare Struktur vor.

_.each = _.forEach = function(obj, iteratee, context) {
  iteratee = optimizeCb(iteratee, context);
  var i, length;
  if (isArrayLike(obj)) {
    for (i = 0, length = obj.length; i < length; i++) {
    iteratee(obj[i], i, obj);
    }
    . . .

Die als Iteratee bezeichnete Funktion wird mit drei Parametern ausgestattet: Als Erstes kommt der Wert des Felds, der gerade zu bearbeiten ist – unser Beispiel nutzte es zur Bevölkerung des Inhalts der anzuzeigenden Messagebox. Parameter zwei ist ein Index, der in das ursprüngliche Feld verweist. Zu guter Letzt wird ein Verweis auf das eigentliche Feld übergeben.

Im Interesse der Kompaktheit soll nicht der komplette Code durchgesprochen werden. Behalten Sie im Hinterkopf, dass die Syntax des Iteratees auch dann identisch bleibt, wenn die Informationen nicht in einem Array, sondern in einem KV-Speicher angeliefert werden.

Mit Namen!

Achten Sie bei der Arbeit mit Underscore.js darauf, dass viele Funktionen unter mehreren Namen ansprechbar sind. So ist die each-Funktion beispielsweise auch unter dem Namen forEach bekannt, der C#-Programmierern geläufiger sein dürfte.

Iteratees sind nicht immer dreiwertig. Ein interessantes Beispiel dafür ist die Funktion reduce, die das an sie übergebene Feld stufenweise „eindampft“ und einen einzelnen Wert zurückliefert. Dieser lässt sich beispielsweise zur Realisierung einer Methode nutzen, die Maximum und Minimum ermittelt:

alert(_.reduce(window.myArray,minF,"X") + " / " +
 _.reduce(window.myArray,maxF,"X"));

Die alert-Funktion wird nun mit Ergebnissen parametriert, die aus zwei Aufrufen von reduce stammen. Diese unterscheiden sich in der übergebenen Berechnungsfunktion. Zudem nutzen wir die Möglichkeit, den Anfangswert des Reduktionsprozesses festzulegen – wir übergeben hier einen String mit einem großen X, um die Berechnungsfunktion darüber zu informieren, dass die Laufvariable noch keine brauchbaren Informationen enthält. Damit können wir uns der eigentlichen Realisierung der beiden Arbeitermethoden zuwenden, die die Intelligenz anliefern. In unserem Beispiel sieht minF folgendermaßen aus:

function minF(memo, value){

  if(memo=="X"){
    memo=value;
  }
  if(value<memo){
    memo=value;
  }
  return memo;
}

Reduktions-Iteratees erhalten als ersten Parameter den aktuellen Stand der Zusammenfassung; als zweiter Parameter wird der einzuschreibende Wert angeliefert. Als Rückgabewert erhält Underscore.js sodann den neuen Wert der Laufvariable.

minF überprüft im ersten Schritt, ob memo einen „gültigen“ Wert enthält. Das Einschreiben eines fremdartigen Startwerts – wir verwenden einen String, während die eigentlichen Nutzwerte stets numerischer Natur sind – ist insofern sinnvoll, als dass die Methode dadurch weiß, ob der angelieferte Wert verwendbar ist oder nicht. Das Übergeben eines sehr großen Werts an minF könnte insofern Probleme erzeugen, als es ja auch ein Feld geben kann, dessen kleinster Wert größer ist als die vom Programmierer nach Belieben ausgewählte Konstante.

Der Rest der Methode ist einfach. Wir prüfen, ob value kleiner ist als der gerade in memo gespeicherte Wert. Ist das der Fall, wird überschrieben. Zu guter Letzt wird das neue Minimalergebnis zurückgeliefert, womit der Prozess in eine neue Runde geht. Im Interesse der Vollständigkeit zeigt Listing 2 noch den Code für die Maximalfunktion, der sich nur insofern von minF unterscheidet, dass statt des Kleiner-Operators in der Selektion nun ein Größer-Operator steht.

function maxF(memo, value){

  if(memo=="X"){
    memo=value;
  }
  if(value>memo){
    memo=value;
  }
  return memo;
  }

Starten Sie das Programm in einem Browser Ihrer Wahl, um sich am Auftauchen der Messagebox zu erfreuen – es ist erwiesen, dass auch diese Version problemlos funktioniert. An dieser Stelle sei auch auf eine kleine Spitzfindigkeit verwiesen: Die soeben besprochene Funktion weist insofern eine Schwäche auf, als die Abarbeitung eines Felds nicht unterbrochen werden kann. Wer each aufruft, ruft den Iteratee mit jedem einzelnen Wert des Arrays auf.

Zur „Umgehung“ dieses Problems bietet sich die Nutzung der find-Methode an. Sie ist an sich darauf spezialisiert, ein Array nach einem bestimmten Wert zu durchsuchen, etwa mit var even = _.find([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; });.

Wer mal mit Palm OS programmiert hat, findet Bekanntes: Die Suche erfolgt über eine Suchfunktion, die in die find-Methode eingeschrieben wird. Sobald sie ein Element für passend befindet, wird die Verarbeitung der Liste gestoppt und das Ergebnis zurückgegeben. Diese Vorgehensweise lässt sich pervertieren: find bricht den Suchprozess ab, wenn die Suchmethode den Wert True zurückliefert. Als Beispiel dafür zeigt Listing 3 eine Methode, die die Bearbeitung der Elemente einstellt, sobald sie den Wert 43 findet.

function findF(whatIsIt){

//Payload
alert(whatIsIt);
//Weiterverarbeitung
if(whatIsIt==43)return true;
return false;

}
window.myArray = [22,33,12,43,12,22,14,15];
_.find(window.myArray,findF);
</script>

Beachten Sie, dass der Korpus von findF in Listing 3 aus zwei Teilen besteht: Erstens der Payload, die mit jedem der Elemente bekanntzumachen ist. Darauf folgt die Überprüfung, ob die Verarbeitung am Ende angelangt ist – wenn Sie das Endelement nicht verarbeiten möchten, können Sie die Reihenfolge der durch Kommentare markierten Felder natürlich vertauschen.

Hat man den zugegebenermaßen seltsamen Kniff einmal durchschaut, ist dieses Programm geradezu primitiv. Die find-Methode wird mit each identisch, wenn die „Suchfunktion“ immer false zurückgeliefert. Ist das Resultat true, wird die Verarbeitung der Schleife beendet.

Im Namen des Felds

Underscore.js enthält mehr als ein Dutzend verschiedener Funktionen, die auf MapReduce-Operationen spezialisiert sind. Wir wollen an dieser Stelle einen Blick auf ein anderes Thema werfen: die Verwertung „klassischer“ Arrays. Hierbei sei angemerkt, dass sich die Arraydefinition von Underscore.js an C und Co. orientiert. Als Sparse Array bezeichnete Felder mit Leerstellen werden laut Dokumentation eher schlecht als recht unterstützt. Bevor wir an dieser Stelle in medias res gehen, wollen wir allerdings noch einen Blick auf eine kleine syntaktische Besonderheit werfen:

_.map([1, 2, 3], function(n){ return n * 2; });
_([1, 2, 3]).map(function(n){ return n * 2; });

Die bisher verwendete Aufrufsyntax nach dem Schema _Funktionsname ist nicht unbedingt erforderlich. Underscore erweitert den globalen Namensraum um die _-Funktion, die als einzigen Parameter das zu verarbeitende Objekt entgegennimmt. Als Rückgabewert erhalten Sie einen „Wrapper“, der die diversen von der Bibliothek exportierten Komfortfunktionen auf eine kompaktere Art und Weise zugänglich macht. Aus technischer Sicht sind die beiden Methoden identisch – wählen Sie einfach die, die Ihren Ansprüchen an sauberen JavaScript-Code eher entspricht.

Ein weiterer lustiger „Verkürzungsaspekt“ ist die Methode der Verkettung. Das Aufrufen der Chain-Methode retourniert ein spezielles Objekt, das „pervertierte“ Versionen der diversen Berechnungsmethoden enthält. Diese unterscheiden sich insofern von ihren gewöhnlichen Kollegen, als dass sie nach jedem Aufruf wieder ein „pervertiertes“ Objekt zurückliefern. Wer den Wert erhalten möchte, muss Value aufrufen:

_.chain(lyrics)
  .map(function(line) { return line.words.split(' '); })
  .flatten()
  .value();

Ob sich die Nutzung von Chain in der Praxis lohnt, sollte von Fall zu Fall entschieden werden: Wer sich mit Underscore.js nicht auskennt, findet diese kompakte Art der Programmierung wahrscheinlich befremdlich und braucht einige Zeit, um den Aufbau des Codes zu verstehen.

Nach diesen syntaktischen Überlegungen wollen wir zur Hauptaufgabe zurückkehren, also der Verarbeitung von Arrays. Als erstes Beispiel wird ein „Parser“ realisiert, der ein angeliefertes Parameterobjekt Schritt für Schritt reduziert. Derartigen Code ndet man in der Praxis übrigens immer wieder: Ein schönes Beispiel dafür wäre die im Qt-Framework enthaltene Funktion arg(). In Underscore.js hört die betreffende Funktion auf den Namen rest. Ihre Verwendung lässt sich anhand eines Beispiels illustrieren:

alert(window.myArray + " / " + _.rest(window.myArray));
</script>

Führen Sie das resultierende Programm aus, um sich am in Abbildung 2 gezeigten Resultat zu erfreuen.

Abb. 2: Jeder Aufruf von „rest“ reduziert die Länge des ArraysAbb. 2: Jeder Aufruf von „rest“ reduziert die Länge des Arrays

Abb. 2: Jeder Aufruf von „rest“ reduziert die Länge des Arrays

Zur Vorführung der auf der Mengenlehre basierenden Funktionen müssen wir im nächsten Schritt ein zweites Array einführen, das einige neue und einige schon bekannte Zahlen enthält. Seine Deklaration sieht folgendermaßen aus:

window.myArray = [22,33,12,43,12,22,14,15];
window.mySecondArray = [91,13,52,43,12,42,13,18];

Damit können wir auch zur Realisierung der unter mathematisch unerfahrenen Personen gefürchteten Differenz und Schnittmengenberechnungen kommen:

var delta=_.difference(window.myArray, window.mySecondArray);
var intersect=_.intersection(window.myArray, window.mySecondArray);
alert(delta + " / " + intersect);

Wer sich mit den Grundlagen der Mengenlehre auskennt, findet im in Abbildung 3 gezeigten Resultat nichts wirklich Neues. Aus systemtechnischer Sicht ist hier vor allem interessant, dass die Mengenlehrefunktionen nicht auf die Verarbeitung von nur zwei Arrays beschränkt sind: Sie können den meisten Methoden auch drei oder vier Elemente übergeben, die sodann der Reihe nach abgearbeitet werden.

Abb. 3: Underscrore.js nimmt der Mengenlehre ihren Schrecken

Abb. 3: Underscrore.js nimmt der Mengenlehre ihren Schrecken

Von besonderem Interesse ist das Verhalten der Differenzfunktion, wenn sie mit mehreren Feldern ausgestattet wird. In diesem Fall wird das erste Feld als „Quelle“ der Werte angenommen, während die anderen Felder als „Eliminatoren“ dienen. Das bedeutet, dass nur jene Felder des Quellarrays in die Ausgabe wandern, die in keinem der anderen Felder vorkommen.

Eine weitere häufige Aufgabe im Leben des Informatikers ist das Finden von Indices, unter denen bestimmte Elemente im Feld anzutreffen sind. Jeremy Ashkenas Implementierung der Suchfunktion indexOf verdient insofern Aufmerksamkeit, weil sie als Best Practice für eigene Implementierungen dienen kann. Beginnen wir mit ihrer Beschreibung in der Normalform:

_.indexOf(array, value, [isSorted])

Die Suche eines Werts in einem Feld ist normalerweise eine Operation, die mehr oder weniger linear mit der Menge der angelieferten Werte wächst. Das ist logisch, muss der Sucher doch alle Elemente analysieren, um einen passenden Kandidaten zu finden.

Liegen die Werte in sortierter Form vor, lässt sich der Suchprozess beispielsweise durch Nutzung eines Binärbaums beschleunigen. Ashkenas trägt dem durch die Einführung des optionalen Werts isSorted Rechnung, mit dem der Entwickler anweisen kann, dass das Array als sortiert zu betrachten ist. Zudem kann isSorted statt true auch einen numerischen Wert übergeben, der einen Startindex beschreibt. Darunterliegende Felder werden von der Suchfunktion schlichtweg ignoriert.

Neben indexOf gibt es auch die Methode last lastIndexOf, die das Array von hinten nach vorne durchsucht. In beiden Fällen gilt, dass sich die Methoden so verhalten, wie man es erwarten würde.

Kontrollierte Ausführung

Entwickler, die erst mit dem Programmieren beginnen, betrachten Funktionen normalerweise als etwas, dem man wenig Aufmerksamkeit schenken muss – braucht man die Methode, ruft man sie auf.

Bei der Arbeit mit JavaScript droht hier in verschiedener Hinsicht Unbill. So ist zum Beispiel der this-Kontext nicht immer das Objekt, das die jeweilige Methode enthält. Eine Besprechung der verschiedenen Kontextveränderungsereignisse würde jedoch den Rahmen dieses Artikels sprengen. Underscore.js schafft insofern Abhilfe, als es das Verbinden einer Funktion und eines Objekts ermöglicht. Betrachten wir hierzu ein aus der Dokumentation übernommenes Beispiel:

var func = function(greeting){ return greeting + ': ' + this.name };
func = _.bind(func, {name: 'moe'}, 'hi');
func();

func() ist hierbei eine Methode, die den anzuzeigenden Wert aus dem gerade aktuellen this-Kontext entnimmt. Durch die Nutzung der with-Methode wird func mit einer Objektinstanz verdrahtet, die als zweiter Parameter an die Methode übergeben wird. Fortan führen alle Aufrufe zur korrekten Ausgabe, weil der Kontext „festgenagelt“ wurde und von der JavaScript Runtime nicht mehr veränderbar ist.

Wenn Funktionen zur Berechnung von Ergebnissen eingesetzt werden, tritt häufig die Situation auf, dass das Übergeben von identischen Parametern zu identischen Werten führt. In diesem Fall lässt sich der oft aufwendige mathematische Prozess dadurch beschleunigen, dass man wie in Abbildung 4 gezeigt eine Art Cache vor die Methode schaltet.

Abb: 4: Dank des vorgeschalteten Zwischenspeichers muss die Berechnungsoperation nicht häufig aufgerufen werden

Abb: 4: Dank des vorgeschalteten Zwischenspeichers muss die Berechnungsoperation nicht häufig aufgerufen werden

Da das manuelle Implementieren in Arbeit ausartet, bringt Underscore.js mit memoize einen Hilfsdienst mit, dessen Nutzung wir uns in den folgenden Schritten kurz ansehen möchten. Als Erstes ist hierzu eine stupide Funktion erforderlich, die neben der Durchführung einer einfachen Berechnung jede Menge Zeit totschlägt, indem sie eine for-Schleife abarbeitet. Der Korpus der Methode, nennen wir sie bess, ist in Listing 4 zu sehen.

function bess(wert)
{
var stoereOptimierung=wert*2;
for(i=0;i<302000;i++)
{
wert=Math.cos(wert)+Math.sin(wert);
}
if(wert>0) return stoereOptimierung / 2;
}

Der auf den ersten Blick geradezu vertrottelt aussehende Aufbau der Funktion ist kein Mittelfinger des Autors an die akademische Zunft: Es handelt sich hier vielmehr um eine Methode die dazu da ist, die Optimierungsfunktionen heutiger JavaScript Runtimes zu umgehen.

Es mag für Quereinsteiger seltsam erscheinen, ist heute aber der Fall: Es gibt mittlerweile sogar im Microcontroller-Bereich Compiler, die die als „Ankerpunkt“ für Breakpoints dienenden tautologischen Zuweisungen erkennen und eliminieren. Unsere hier verwendete Methode ist nervtötend komplex und sollte auch für fortgeschrittene Optimierungsalgorithmen unangreifbar sein – wenn Sie einen Browser besitzen, der die Schleife wegoptimiert, freut sich der Autor dieser Zeilen über einen Leserbrief.

Wie dem auch sei, wir können nun die Ausführungsdauer der bess-Methode berechnen. Da wir davon ausgehen, dass die Abarbeitung zumindest einige Millisekunden in Anspruch nimmt, ist die Nutzung des Date-Objekts ausreichend. Damit können wir eine Gruppe von bess-Operationen anstoßen, wie Listing 5 zeigt.

var oldT= new Date();
bess(22);
var newT= new Date();
alert(newT-oldT);
oldT= new Date();
bess(22);
newT= new Date();
alert(newT-oldT);
oldT= new Date();
bess(22);
newT= new Date();
alert(newT-oldT);

Aus systemtechnischer Sicht ist das keine Raketenwissenschaft: Wir speichern den Wert von Date vor und nach der Ausführung in zwei Variablen, ermitteln die Differenz und geben sie sodann aus. Die drei hintereinander auszuführenden Durchläufe von bess ergeben auf der Workstation des Autors Werte zwischen 23 und 41 Millisekunden. Wundern Sie sich nicht darüber, dass sie stark divergieren. Das Durchführen von Mikrobenchmarks ist eine Wissenschaft für sich, auf die wir an dieser Stelle aber nicht eingehen können.

Im nächsten Schritt wollen wir die Ausführung der Funktion beschleunigen, indem wir die memoize-Methode von Underscore.js einsetzen:

var smartBess = _.memoize(bess);
var oldT= new Date();
smartBess(22);
var newT= new Date();
alert(newT-oldT);
oldT= new Date();
smartBess(22);

Ein weiterer Weg zur Reduktion der Rechenlast – das ist vor allem im Interesse der Akkulaufzeit wichtig – besteht darin, die Ausführungshäufigkeit von Funktionen zu reduzieren.

Nach dem Aufruf von memoize liefert Underscore.js einen neuen Funktionspointer zurück, der auf die mit dem Cache versehene Funktion verweist. Bei Aufrufen wird im ersten Schritt der Cache untersucht – ist der passende Wert schon einmal berechnet worden, wird er statt einer neuen Berechnung zurückgeliefert. In Tests des Autors führte das zu einer massiven Beschleunigung der Ausführung von bess: Der erste Durchlauf dauerte zwar nach wie vor rund 25 ms, die anderen beiden erfolgten allerdings so schnell, dass sie unter der Messgenauigkeit von Date lagen.

Falls Sie bestimmte Ansprüche an die zu verarbeitenden Daten stellen, können Sie der Methode eine eigene Cache-Funktion einschreiben. Achten Sie allerdings darauf, dass memoize selbst keinerlei Analyse des in der Funktion enthaltenen Bytecode durchführt: Prüfen Sie dort beispielsweise eine externe Variable, die sich zwischen den Aufrufen ändert, ist Ärger vorprogrammiert.

Ein weiterer Weg zur Reduktion der Rechenlast – im Computermarkt ist das im Interesse der Akkulaufzeit wichtig – besteht darin, die Ausführungshäufigkeit von Funktionen auf verschiedene Arten zu reduzieren. Ashkenas lies sich bei der Implementierung dieser Einschränkungsfunktion vom Elektronikbereich inspirieren – die in Abbildung 5 sichtbare Schaltung zeigt, woran er dachte.

Abb. 5: Drückt der Nutzer auf einen Taster, kommen ob eines als Prellen bezeichneten Phänomens mehrere Impulse beim Prozessrechner an

Abb. 5: Drückt der Nutzer auf einen Taster, kommen ob eines als Prellen bezeichneten Phänomens mehrere Impulse beim Prozessrechner an

Zum Test dieses Verhaltens wollen wir einen neuen Testharnisch realisieren: Er besteht aus einem Button, der mit einem Ereignis-Handler verbunden ist. Der Handler ändert seinerseits die Farbe des div-Tags, wann immer ein Event eintrifft. Eine einfache Implementierung des Markup-Teils ist in Listing 6 zu sehen.

<html> 
<head> 
  <script src="underscore.js"></script> 

</head> 
<body> 
  <div id="worker" style="width: 960px; background-color: pink;"> 
    <p>-x-<br>-x-<br>-x-<br>-x-<br>-x-</p> 
  </div><br> 
  <button onclick="toggleMe()">Click me</button> 
    <script type="text/javascript"> </script> 
</body> 
</html>

Da das Inkludieren von jQuery in dieses kleine Demoprogramm ein absoluter Overkill wäre, beschränken wir uns stattdessen auf eine Vanilla-JavaScript-Implementierung der CSS-Änderungsfunktion, wie in Listing 7 gezeigt.

function toggleMe() {
  rnd=Math.random();
  if(rnd<0.2) {
    document.getElementById("worker").style.backgroundColor = "blue";
  }
  else if(rnd<0.4) {
    document.getElementById("worker").style.backgroundColor = "green";
  }
  else if(rnd<0.6) {
    document.getElementById("worker").style.backgroundColor = "red";
  }
  else if(rnd<0.8) {
    document.getElementById("worker").style.backgroundColor = "darkmagenta";
  }
  else {
    document.getElementById("worker").style.backgroundColor = "chocolate";
  }
}

Aus technischer Sicht ist hier nur die Nutzung des Zufallsgenerators interessant: Wir hoffen darauf, dass es bei jedem Anklicken zu einer Änderung der angezeigten Farbe kommt. Dieser auf den ersten Blick vernünftig aussehende Code kommt insofern an seine Grenzen, als dass User mitunter sehr häufig auf den Button klicken können. Wir wollen in den folgenden Schritten davon ausgehen, dass dies nicht gewünscht ist. Der erste Weg zur Umgehung des Problems ist die Nutzung der Methode throttle (Listing 8).

function worker() { 
  rnd=Math.random(); 
  if(rnd<0.2) { 
    document.getElementById("worker").style.backgroundColor = "blue"; 
  }. . . 
} 
window.worker= _.throttle(worker, 3000); 
function toggleMe() 	{ 
  window.worker(); 
} 
  </script>

Ashkenas orientiert sich bei der Implementierung an der weiter oben besprochenen memoize-Funktion: Nach dem Aufruf wird ein neuer Funktions-Pointer zurückgeliefert, der die ursprüngliche Funktion und etwas von Underscore.js bereitgestellte Logik enthält. Der numerische Parameter legt hierbei fest, wie oft die Funktion aufgerufen werden darf – wir übergeben hier 3 000, um eine Mindestabkühlphase von 3 000 Millisekunden anzufordern. An dieser Stelle bietet sich ein Test an: Laden Sie die Webseite in einem Browser Ihrer Wahl und klicken Sie fleißig auf den Button. Die Änderung der Farbe des Tags erfolgt nun sofort nach dem ersten Klick, um danach langsamer zu werden. Dieses Verhalten lässt sich übrigens anpassen – das sofortige erstmalige Abfeuern ist nicht unbedingt notwendig.

debounce – der englische Name steht für Entprellen – ist konzeptionell eng mit throttle verwandt. Die Methode unterscheidet sich insofern davon, als sie die ihr übergebene Payload erst dann aufruft, wenn die Wartezeit verstrichen ist. Im Fall unseres Beispiels würde das bedeuten, dass mehrfaches Anklicken des Buttons anfangs ignoriert wird – die Änderung der Farbe erfolgt erst, nachdem Ruhe eingekehrt ist:

window.worker= _.debounce(worker, 500); 
  function toggleMe() { 
  window.worker(); 
}

Der wichtigste Unterschied zu throttle ist, dass erstere Funktion bei regelmäßig auftretenden Ereignissen regelmäßig Aufrufe der Funktion absetzt – bei debounce wird erst dann invoziert, wenn die Wartezeit um ist.

Fazit

Underscore.js ist ein wahres Kraftpaket: Die gerade mal sechs Kilobyte große Bibliothek bietet derartig viele Funktionen, dass es unmöglich ist, sie in einem einzelnen Artikel zu beschreiben. Damit hätte man das ganze Heft füllen können, also musste ich eine Auswahl treffen.

Wer nicht unter dem „Not-invented-here-Syndrom“ leidet, sollte die aus didaktischer Sicht exzellent aufgebaute Dokumentation konsultieren. Behalten Sie die Bibliothek sodann im Hinterkopf. Es kann immer wieder vorkommen, dass Sie in der Praxis ein Codestück aus ihr gut gebrauchen können.

PHP Magazin

Entwickler MagazinDieser Artikel ist im PHP Magazin erschienen. Das PHP Magazin deckt ein breites Spektrum an Themen ab, die für die erfolgreiche Webentwicklung unerlässlich sind.

Natürlich können Sie das PHP Magazin über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist das Entwickler Magazin ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

X
- Gib Deinen Standort ein -
- or -