ECMAScript 6: Generators

Generators sind eins der Features von ECMAScript 6, die erhebliche Auswirkungen auf die tägliche JS-Programmierpraxis haben werden. So werden durch Generators vor allem asynchrone Operationen zum Kinderspiel; also genau jene Teile des JS-Alltags, die heutzutage im besten Fall nur mit lästiger Fummelei verbunden sind und im schlimmsten Fall mit dem Abgleiten in die callback hell enden. Da mittlerweile die ersten Browser und auch Node.js Generators implementieren, lohnt sich ein gründlicher Blick auf dieses neue Werkzeug bereits heute, denn wie gesagt: asynchrone Programmierung wird durch Generators extrem vereinfacht. Alles, was vorher in einer ünübersichtlichen Callback-Verschachtelung endete, kann mit dem neuen ES6-Feature und einem altbekannten JS-Pattern in übersichtlichen Sequenzen ausgedrückt werden. Klingt sehr kompliziert, ist es aber eigentlich gar nicht.

Was ist ein Generator?

Das ECMAScript-Wiki beschreibt Generators wie folgt:

First-class coroutines, represented as objects encapsulating suspended execution contexts (i.e., function activations).

In irdischer Sprache formuliert handelt es sich um pausierbare Funktionen. Diese Funktionen (bzw. die aus ihnen erstellten Objekte) können eine Anzahl von Werten ausgeben und ihrerseits mit neuen Werten gefüttert werden. Jede Ausgabe eines Werts pausiert die Funktion, jede Fortsetzung der Funktion bietet die Möglichkeit, einen neuen Wert in die Funktion einzugeben.

Ein Generator ist das Produkt einer generator function, einer JS-Funktion mit einer speziellen Syntax:

// Generator Function
var genFn = function*(){
};

// Generator
var gen = genFn();

Hier ist genFn die generator function und gen der Generator. Eine generator function also ist eine Vorlage, die einen Generator produziert. Sie unterscheidet sich durch zwei Merkmale von einer herkömmlichen Funktion; neben dem * hinter function kann in ihr yield anstelle bzw. als Ergänzung zu return verwendet werden.

Aus dem Generator …

Wird auf einem Generator-Objekt die next()-Methode aufgerufen, wird der Code der erstellenden generator function ausgeführt, bis das erste yield erreicht wird. Dieses neue Schlüsselwort ist wie return, in dem Sinne dass es einen Wert zurückgibt. Allerdings ist mit dem ersten yield die Funktion nicht beendet, sondern nur an exakt dieser Stelle pausiert. Am besten versteht man das an einem super-simplen (und entsprechend nutzlosen) Beispiel:

// Generator Function
var genFn = function*(){
  yield 1;
  yield 2;
  yield 3;
};

// Generator
var gen = genFn();

Ein Aufruf von gen.next() führt dazu, dass die generator function ausgeführt wird, allerdings nur bis zum ersten yield. Das yield gibt vergleichbar mit return einen Wert zurück. Im Unterschied zu return beendet yield allerdings die weitere Ausführung der Funktion nur vorläufig. Die Funktion merkt sich, an welcher Position im Code das yield stattgefunden hat und beim nächsten Aufruf von next() macht sie an genau dieser Stelle weiter. Man kann also gen.next() drei mal aufrufen und bekommt jedes mal einen anderen Wert, weil der Reihe nach alle drei yield drankommen:

var genFn = function*(){
  yield 1;
  yield 2;
  yield 3;
};
var gen = genFn();

gen.next(); // > { value: 1, done: false }
gen.next(); // > { value: 2, done: false }
gen.next(); // > { value: 3, done: false }
gen.next(); // > { value: undefined, done: true }
gen.next(); // > Exception: Generator has already finished

Der Wert, den next() zurückgibt, ist ein Objekt, dass nebem dem Wert in der Eigenschaft value auch ein done-Flag enthält, das angibt, ob der Generator komplett abgearbeitet wurde. Nach drei Aufrufen von next() sind alle yield drangekommen und entsprechend gibt es keinen Wert mehr, sondern nur noch undefined mit dem Hinweis done: true. Ein weiterer Aufruf next() wird mit einer Exception quittiert.

Mit Generatoren lassen sich allerlei Dinge anstellen, die man anders in JavaScript nicht auf die Reihe bekommt. Eine endlose (wirklich endlose) Zahlenreihe zu repräsentieren ist mit ihnen zum Beispiel ein Kinderspiel:

var genFn = function*(){
  var i = 0;
  while(true){
    yield i++;
  }
};
var gen = genFn();

Hier kann man gen.next() aufrufen bis man grün wird, die Zahlenreihe endet nie. Gleichmaßen führt die Generator-Funktion selbst nicht zu einer Browser-Blockade, da die while-Schleife nach jedem Durchlauf mit yield verlassen und erst bei gen.next() wieder betreten wird. Somit ist auch klar, warum wir hier von generators sprechen – die generator function beschreibt eine Sequenz und jedes Mal wenn wir das aus ihr produzierte Objekt anstoßen, wird ein entsprechender Wert generiert.

Für Dinge wie endlose Zahlenreihen brauchen wir auch nicht mehr zum Thema Generator zu wissen. Wer aber jenseits der Fibonacci-Zahlen noch Use Cases für den Browser-Alltag sucht, sollte aber wissen, dass man sich ein Generator auch gerne mit neuen Inputs füttern lässt.

… in den Generator

Was yield von return unterscheidet ist neben der eingebauten Hier-Gleich-Weitermachen-Funktionalität auch, dass man von außen an die Stelle von yield einen Wert in den Generator hineinwerfen kann. Im folgenden Codebeispiel sorgt der erste Aufruf von next() dafür, dass die Funktion bis zum ersten yield kommt, wo 23 zurückgegeben wird. Der nächste next()-Aufruf erhält mit 42 einen Wert, und da die Funktion direkt nach dem vorherigen yield fortgesetzt wird, wird dieser Wert an die Stelle des vorher zurückgebenen gesetzt; im Prinzip steht dann dort die Zeile i = 42. Entsprechend ist das, was schließlich mit return zurückgegeben wird, der Wert 42.

var genFn = function*(){
  var i = yield 23;
  return i;
};
var gen = genFn();

// Ausführung bis zum yield; 23 wird durch yield ausgegeben
gen.next();   // > { value: 23, done: false }

// Fortsetzung ab dem yield; 42 wird an die Stelle des yield eingegeben
gen.next(42); // > { value: 42, done: true }

Ein Generator kann also nicht nur einen Wert zurückgeben und dann pausieren, wir können auch bestimmen mit welchem Wert der Generator weitermacht. Dieses Feature erlaubt es, aus einem Generator einen Wert auszuwerfen, den Wert zu transformieren und ihn dann wieder in den Generator hineinzustecken, der dann mit dem transformierten Wert fortfährt. Man könnte zum Beispiel einen Generator Objekte auswerfen lassen, die von der Außenwelt auf einen Wert reduziert und wieder in den Generator hineingeworfen werden:

var genFn = function*(){
  var sum = yield [1, 2, 3];
  var factor = 2;
  return sum * factor;
};

var gen = genFn();

var arr = gen.next().value;
var sum = arr.reduce(function(x, y){
  return x + y;
});
var result = gen.next(sum).value; // > 12

Der Generator wirft ein Array aus, das außerhalb des Generators zu einer einzigen Zahl reduziert und wieder in Generator hineingesteckt wird. Da dieses Hineinstecken genau an der Stelle des yield passiert, könnte man sagen dass sich der Ausdruck sum = yield [1, 2, 3] in sum = neuerWert verwandet; so erhält sum als Wert die Zahl, die außerhalb des Generators aus dem Arrays errechnet wurde. Durch diese Zuweisung an sum kann der Generator den außen errechneten Wert dann weiterverwerten, ihn mit dem factor multiplizieren und das finale Endergebnis ausgeben.

Besonders sinnvoll mag dieses Beispiel nicht erscheinen, was aber vor allem daran liegt, dass es eine ganz wesentliche Eigenschaft von Generators gar nicht benutzt; die Pause-Funktion! Das Reduzieren des Arrays könnte ja theoretisch auch eine lang dauernde, asynchrone Operation sein. Wenn dies der Fall wäre, würde das aber den Code der generator function gar nicht berühren:

var genFn = function*(){
  var sum = yield [1, 2, 3];
  var factor = 2;
  return sum * factor;
};

var gen = genFn();

var arr = gen.next().value;

// Asynchrones errechnen des Wertes aus dem Array
setTimeout(function(){
  var sum = arr.reduce(function(x, y){
    return x + y;
  });
  var result = gen.next(sum).value; // > 12
}, 1000);

Die durch die generator function beschriebene Sequenz bleibt, wie sie ist; dass anderswo asynchrone Operationen passiert, ist egal, denn nach dem yield ist der Generator schließlich pausiert. Er wartet auf das nächste next(), bevor er seine Berechnung zum Ende bringt. Wenn man das zuende denkt, könnte man Code formulieren, der eine generator function so verarbeitet, dass der Code der Funktion lediglich eine Abfolge von asynchronen Operationen darstellt und die Aufrufe von next() automatisch passieren. In unserer täglichen Arbeit schreiben wir also nur noch so etwas …

async(function*(){
  var wert1 = yield macheWasAsynchrones();
  var wert2 = yield macheWasAnderesAsynchrones();
  whatever(wert1 + wert2);
});

… und die async()-Funktion kümmert sich um den Rest. Wir können uns damit von der callback hell für immer verabschieden und es ist noch nicht mal schwer: wir müssen nur das bisher über Generators gelernte mit althergebrachten Promises kombinieren.

Rein-Raus-Ajax via Generator

Startet man einen Ajax-Request mit jQuerys $.get() so gibt diese Funktion ein Objekt zurück, das die asynchrone Operation kapselt. An dieses Objekt lassen sich Callbacks hängen, die feuern sobald die Ajax-Operation abgeschlossen ist.

var operation = $.get('/api/foo');
operation.then(function erfolgCallback(data){
  console.log('Yay!', data);
}, function failCallback(){
  console.log('Ups!');
});

Diese Objekte gibt es nicht nur bei Ajax-Requests und nicht nur in jQuery, sondern sehr viele JS-Libraries können derartiges liefern. Diese promises genannten Objekte abstrahieren alle Arten von asynchrone Operationen hinter einer immergleichen API; sie alle produzieren Objekte mit einer then()-Methode und triggern Callbacks wenn die hinter den Kulissen ablaufende Operationen beendet werden. Objekte und asynchrone Operationen? Das klingt nach einem Job für einem Generator! Die generator function müsste einfach Promises auswerfen …

var genFn = function*(){
  $('#Alpha').text(yield $.get('/api/alpha'));
  $('#Beta').text(yield $.get('/api/beta'));
  $('#Gamma').text(yield $.get('/api/gamma'));
};

… die durch externen Code aufgelöst und deren Resultate zurück in den Generator gegeben werden:

function async(genFn){
  var gen = genFn();
  var resume = function(promise){
    return promise.done(function(text){
      var next = gen.next(text);
      if(!next.done){
        return resume(next.value);
      }
    });
  };
  return resume(gen.next().value);
}

Fertig! (funktioniert Stand Ende 2013 in Chrome mit aktiviertem Experimentelles-JS-Flag und Firefox-Vorabversionen)

Die async()-Funktion nimmt eine generator function und erstellt aus ihr den Generator gen. Die resume()-Funktion nimmt ein Promise (bezogen aus gen.next().value) und hängt dort mittels done() einen Callback an. Feuert der Callback, wird dessen erster Parameter in den Generator hereingesteckt und sofern der Generator nicht schon sein letztes yield hinter sich hatte das nächste Promise in resume() gesteckt. So fertigt async() Schritt für Schritt die Generator-Sequenz ab, ganz ohne dass der Autor der Sequenz auch nur einen Moment lang über Callbacks nachdenken müsste, vorausgesetzt die yields im Generator geben immer Promises zurück.

In ihrer jetzigen Form ist die async()-Funktion freilich noch ausbaufähig; zum Beispiel hat sie noch keine Zeile für den Fall, dass eine der Async-Operationen fehlschlägt. Fehlerbehandlung nachzurüsten ist aber kein Problem, denn die Generator-API hat neben next() noch ein paar weitere nützliche Funktionen.

Weitere Generator-Funktionen

Neben next() bieten Generators auch eine throw()-Methode. Diese erlaubt es, einen Wert in den Generator zu werten, an der Stelle des letzten yield eine Exception auslöst. So kann man problemlos Fehler, die in asynchronen Operationen außerhalb des Generators passieren an die das Problem auslösende Stelle im Generator zurückführen: und schon funktioniert try-catch mit asynchronem Code!

var genFn = function*(){
  $('#Alpha').text(yield $.get('/api/alpha'));
  $('#Beta').text(yield $.get('/gibt/es/nicht/404')); // Exception tritt hier auf
  $('#Gamma').text(yield $.get('/api/gamma'));
};

function async(genFn){
  var gen = genFn();
  var resume = function(promise){
    return promise.done(function(text){
      var next = gen.next(text);
      if(!next.done){
        return resume(next.value);
      }
    }).fail(function(err){
      gen.throw(err.status);
    });
  };
  return resume(gen.next().value);
}

try {
  async(genFn);
} catch(e){
  console.log(e);
}

Außerdem können Generators mit den in ES6 ebenfalls neuen neuen for-of-Schleifen benutzt werden (Demo):

var genFn = function*(){
  var i = 0;
  while(i < 100){
    yield i++;
  }
};

var gen = genFn();

for(var num of gen){
  console.log(num);
}

Die for-of-Schleife ist ein universeller Iterationsmechanismus, mit dem in ES6-JS nicht nur Generators, sondern alle möglichen Sorten von Objekten verarbeitet werden können. Das Iterator-Konzept ist allerdings ein eigenes Thema für einen späteren Zeitpunkt – spannender dürfte aktuell die Frage sein, wie es denn um die Implementierung von Generators in heutigen JavaScript-Engines bestellt ist.

Unterstützung für Generators

Native und weitgehend standardkonforme Unterstützung für Generators findet sich Stand Ende 2013 in Chrome (vorausgesetzt der Exterminelles-JavaScript-Flag wurde in den Einstellungen aktivieren), in Firefox-Vorabversionen und in mit dem Parameter --harmony gestartenen Node-Umgebungen. Außerdem kann die Generator-Syntax nebst allen APIs vom Traceur-Compiler in JavaScript übersetzt werden, das jeder heutige Browser versteht. Erwartungsgemäß vervielfacht sich durch den Übersetzungsprozess die Code-Menge aber im Prinzip gilt: Generators funktionieren!

Fragen zu HTML5 & Co beantwortet 12 - Formularvalidierung, Media-Attribute, H1-Headlines, Main-Element

Ein letztes Mal im Jahr 2013 darf ich für euch Spezifikations-Exegese betreiben und Fragen zu HTML5 beantworten. Falls euch noch mehr brennende Fragen quälen (gerne auch CSS und JavaScript), dann schreibt mir eine E-Mail oder gebt die Frage per Twitter ab.

Bedingtes Required-Feld mit HTML5?

Kennt HTML5 für Formulare auch ein abhängiges required, dass z.B. ein Feld ist nur dann ein Pflichtfeld ist, wenn eine bestimmte Checkbox aktiviert wurde? Oder bleibt einem hier nur JavaScript?

Ohne JavaScript geht das nicht – „wenn A, dann B“ ist schon Programmierung und kein Markup mehr. Aber man kann das JS so gestalten, dass es sich in die Validierungsmechanik von HTML5 einklinkt. In konkreten Fall wäre es einfach damit getan, das required-Attribut auszutauschen, je nachdem ob die Checkbox angehakt ist oder nicht. Wichtig ist nur, dass das in dem Moment passiert, in dem sich die Checkbox ändert und nicht erst beim Absenden des Formulars.

Für die komplexeren Fälle ist es aber noch gut zu wissen, dass dank HTML5 Formularfelder eine JS-Methode namens setCustomValidity() haben, die es erlauben, eine Fehlermeldung so einzubinden, dass sie genau so funktioniert, wie die Standard-Meldungen. Eine kleine Demo gibt es hier.

media-Attribut für link-Elemente

Kann man bei HTML5 das media-Attribut für Stylesheets im link-Element weglassen? Bei www.peterkroener.de und bei HTML5 Boilerplate wird nämlich kein media-Attribut angegeben. Gibt es Nachteile, wenn man das media-Attribut nicht benutzt?

Die HTML5-Spezifikationen bezeichnen die Angabe des media-Attributs für Stylesheet-Links als prescriptive, legt aber auch fest, was passiert wenn das Attribut fehlt: in dem Falle gilt der Media Query all (Link). Insofern gibt es keine überraschenden Nachteile wenn das Attribut fehlt und durch ein Auslassen des Attributes wird das HTML wird auch nicht ungültig.

Ich könnte sogar ein Argument für das Auslassen des media-Attributs finden. Browser halten sich durchaus an das Attribut, d.h. sie wenden nur jene Styles an, auf die der Media Query im Attribut zutrifft. Sie laden aber immer alle in link-Elementen referenzierten Stylesheets herunter und schauen erst dann, ob sie die auch gebrauchen können. Überflüssige Downloads sind doof – besser ist es meist, alle Stylesheets in eine Datei zu packen und dort mit Media Queries zu arbeiten. Lässt man dann dann media-Attribut bei den link-Elementen aus, kommen weniger informierte Zeitgenossen gar nicht erst auf die Idee, der Browser würde die referenzierte Datei nicht herunterladen. Ansonsten ist es aber wirklich egal, ob dieses Attribut da ist oder nicht.

Ist <main> einsatzbereit?

Wie ist heute der Stand bezüglich des <main>-Elements? Kann man es bereits ohne Kompromisse einsetzen?

Grundsätzlich einsetzbar ist das neue Element (Details) schon und mit dem html5shiv funktioniert es auch in allen älteren Browsern. Noch sind sich W3C und WHATWG-Spezifikationen aber nicht in allen Details einig (Stand Ende 2013). Laut W3C darf es nur ein <main> pro Seite geben; es markiert den Hauptinhalt der gesamten Seite. Die WHATWG findet hingegen, dass man auch mehrere <main> haben können sollte, womit jeweils der Hauptinhalt des Umgebenden Container-Elements (z.b. <section>) ausgezeichnet wird.

Dass das neue Element kommt, scheint außer Frage zu sein, es geht nur noch um das wie. Und da der Use Case „Hauptinhalt der gesamten Seite“ in jedem Fall durch die Spezifikationen abgedeckt wird, hätte ich zumindest diesbezüglich gar keine Bedenken. Aber wie immer gilt: in Bezug auf HTML5-Markup immer schön locker bleiben und nicht überstürzt vorhandene, funktionierende Webseiten umbauen nur weil irgendwer etwas neues erfunden hat.

Mehr als eine H1 pro Seite?

Kann ich mehr als eine <h1> in ein HTML5-Dokument schreiben ohne mit <article> o.Ä. zu separieren? Einfach so?

Es spricht aus rein semantische Sicht nichts dagegen. Wenn es mehrere Überschrifen gleicher Ebene innerhalb eines Abschnitts gibt, dann sind das eben Überschriften mit gleichem Rang (Specs). Und nirgends steht geschrieben, dass das nicht nicht auch für <h1> gilt. Bevor man aber wirklich mehrere <h1>-Elemente in der Seite verteilt, sollte man die SEO-Abteilung konsultieren, denn Suchmaschinen-Nerds haben oft sehr spezielle Meinungen zu Überschrift-Elementen allgemein und zu <h1> im Besonderen.

Weitere Fragen?

Eure Fragen zu HTML5, JavaScript und anderen Webtechnologien beantworte ich gerne! Einfach eine E-Mail schreiben oder Twitter bemühen und ein bisschen Geduld mit der Antwort haben. Ansonsten kann man mich natürlich auch mieten.

Hardware-Review: Samsung 900X3E K06 ATIV Book 9 (in Tateinheit mit Ubuntu)

Mein (gar nicht mal so) alter Laptop ist einigen Jahren des Dauereinsatzes physisch ganz schön am Ende. Nachdem das Ding im wirklich Wochentakt von einer Ecke des Landes an die andere geschleppt wurde ist so manches Scharnier und so mancher Anschluss ausgeleiert, eine Akkuzelle ist tot und der Sugru-Anteil an der Gesamtmasse steigt so langsam auf bedenkliche Werte. Außerdem reicht die noch heile Hardware kaum aus, um die abgefahreneren HTML5-Demos wie z.B. Epic Citadel ruckelfrei abzuspielen. Zeit also für ein neues Gerät! Gesucht war ein Ultrabook mit ordentlich Power, das linuxfreundlich und so klein und leicht wie möglich sein sollte. Geworden ist es das vollkommen bescheuert benannte Samsung 900X3E K06 ATIV Book 9 (Amazon ), ein Nachfolgemodell des bisher von mir eingesetzen (und ebenso bescheuert benannten) Samsung NP900X3A A01. Auf Twitter wurde mehrfach ein Review verlangt, das ich hiermit nach den ersten Testeinsätzen in den letzten Woche liefere.

Eigentlich macht das ATIV Book 9 alles, was ich brauche. Es hat auch für die verrückteste WebGL-Demo genug Power (i7-3537U, Intel HD 4000, 4 GB RAM) und ist dabei klein (313,8 x 218,5 x 12,9 mm) und wiegt nur 1,16 kg. Das ist noch mal ein kleine Stück kleiner und leichter als das Vorgängermodell, das von den Abmessungen her eine 1:1-Entsprechung des MacBook Air war.

Das Samsung 900X3E K06 ATIV Book 9a ist sehr klein und leicht

Das Gehäuse ist komplett aus Metall, macht einen soliden Eindruck und sieht für einen PC auch gar nicht mal so schlimm aus. Ubuntu 13.04 ließ sich widerstandslos an die Stelle des vorinstallierten Windows 8 setzen und alles wichtige funktioniert aus dem Stand, ähnlich wie bei den Vorgängermodellen. Außer Betrieb ist bei mir im Moment die Regelung der Tastaturbeleuchtung und hin und wieder berichten nach dem Standby einzelne Indikatoren Unfug – z.B. behauptet angeschaltetes Bluetooth dass es aus ist und die Batterie zeigt nicht immer an, ob sie gerade be- oder entladen wird. Ich nehme an, dass dieser Kleinkram mit ein bisschen Frickelei zu reparieren ist oder sich mit der neuen Ubuntu-Version von selbst behebt.

Wie bei den Vorgängermodellen fällt das Ultrabook-typische, spartanische Anschluss-Angebot aus: neben 2× USB, 1× Micro-HDMI, einer kombinierten 3,5 mm-Audiobuchse und einem Kartenslot gibt es noch einen Anschluss für einen (mitgelieferten) proprietären RJ45-Adapter. Das ist nicht ungewöhnlich, aber auf der linken Seite sind die Anschlüsse so dicht aneinander gepackt, dass man dort außer dünnsten USB-Steckern nicht viel anschließen kann, wenn man nebenbei auch Strom und HDMI-Output haben möchte. Das folgende Bild zeigt die linke Seite bei voller Belegung  der nun wirklich nicht besonders dicke nicht angeschlossene Speicherstick würde sich nur mit sanfter Gewalt anstelle des USB-Kabels anschließen lassen.

Sehr beengte Platzverhältnisse bei den Anschlüssen des Ultrabooks

Als Star der Show dürfte das Display durchgehen. Es ist matt (wichtig!) und sportet mit 1.920 x 1.080 eine stattliche Auflösung. Was sich mit dieser Auflösung anstellen lässt, ist eine Software-Frage. Moderne Betriebssysteme wie Windows 8.1 sind wohl in der Lage, die Auflösung in Bildschärfe umzusetzen statt einfach alles auf dem 13,3"-Bildschirm kleiner erscheinen zu lassen. Ob man das braucht, ist wohl eine Geschmacksfrage und ob Ubuntu das kann, habe ich nicht erforscht. Mir gefällt die normale hohe Auflösung ausgesprochen gut.

Scharfes Display mit hoher Auflösung

Wo wurde gespart und geschlampt? Die eingebauten Lautsprecher sind natürlich nichts tolles, aber auch nicht so schlimm wie schon in anderen Laptops gehört. Die Webcam reißt ebenfalls keine Bäume aus. Und man darf natürlich nicht vergessen, dass das Gerät richtig schön teuer ist. Samsungs UVP liegt bei 1.999 € und obwohl ich mit etwas unter 1.700 € davongekommen bin, ist das nicht gerade billig. Da es an der Video-Ausgang-Front nur Micro-HDMI gibt, darf man ggf. noch zusätzlich für z.B. VGA-Adapter nicht unerhebliche Summen löhnen (ich nutze den Vorgänger dieses Teils). Fazit?

Pro:

  • Klein, leicht, flott
  • Mattes, sehr hoch auflösendes Display
  • Ubuntu 13.04 läuft aus dem Stand so gut wie perfekt

Contra:

  • Sehr eng aneinanderliegende Anschlüsse
  • Teuer
  • Ggf. müssen nicht ganz billige Micro-HDMI-Adapter gekauft werden

Für meinen Use Case (Dauermobil-Einsatz mit Ubuntu) scheint das Gerät wie gemalt zu sein. Den größten Haken würde ich eindeutig an der Kostenfront ausmachen. Man bekommt wenig Gerät (d.h. wenig Größe und Gewicht) mit angemessen viel Power für sehr viel Geld. Wer willens und in der Lage ist, etwas mehr zu schleppen oder auf das Monster-Display zu verzichten, wird sicher auch mit weniger Finanzaufwand glücklich.

Unsync: synchrone JavaScript-Funktionen im Browser einfach asynchron machen

In letzter Zeit hab ich hin und wieder Dinge gebastelt, die schwergewichtige Rechenaufgaben in Browsern durchführen. Dank HTML5 gibt es hierfür ja mehr als genug Anlässe, wobei mein konkretes Anliegen die Analyse von Bilddaten aus Webcam-Feeds war. Das Grundproblem bei so etwas ist, dass lang laufende Scripts (z.B. für Gesichtserkennung) den Browser komplett blockieren können. In meinem Fall ruckelte meist nur das Video, aber der Extremfall dieses Problems ist das allseits bekannte Ein-Script-auf-dieser-Seite-läuft-zu-lange-Popup. All das will man nicht haben und eigentlich hat HTML5 auch hierzu eine Lösung parat. Das Dumme an der Lösung ist, dass ihre Benutzung recht viel Aufwand bedeutet, so dass ich etwas drumherum konstruiert habe, was die Angelegenheit in vielen Fällen auf einen JavaScript-Einzeiler reduziert: Unsync.

Web Workers werden gerne als „Threads für JavaScript“ beworben. Dieses Label ist auch nicht falsch, aber ob Web Workers ihren Job durch Threads oder mit Mitteln schwarzer Magie ausführen, ist eigentlich egal. Der interessante Effekt von Workers ist, dass sie synchronen Code asynchronisieren und die Blockade des Browsers verhindern. Dieses JS-Snippet bringt (Stand Mitte 2013) selbst die neueste Chrome-Beta ins Schwitzen:

for(var i = 0; i < 50000000; i++){
  Math.sqrt(Math.random() * 1000000000);
}
window.alert('Fertig!');

Bevor die Fertig-Meldung kommt, müssen wir mehrere Sekunden einen blockierten Browser ertragen. Web Workers erlauben es, die Blockade zu lösen. Dazu muss man den zeitkritischen Code in eine Extra-JS-Datei verfrachten und diese Extra-Datei durch einen Worker laden lassen:

// Haupt-JS-File
var myWorker = new Worker('worker.js');
myWorker.postMessage(null); // Startschuss geben
myWorker.onmessage = function(){ // Fertig-Meldung empfangen
  window.alert('Fertig!')
};

// worker.js
this.onmessage = function(){ // Startschuss empfangen
  for(var i = 0; i < 5000000000; i++){
    Math.sqrt(Math.random() * 1000000000);
  }
  this.postMessage(null); // Fertig-Meldung abschicken
};

Diese Lösung finde ich reichlich unzumutbar. Warum sollte man denn bitte den Code für die Rechenaufgabe in eine Extra-Datei verfrachten wollen? Code modularisiert man bitteschön nach rein inhaltlichen Gesichtspunkten festgelegten Grenzen, die Performance darf da nichts zu melden haben. Das ganze Gewurschtel mit Events und postMessage() könnte auch bequemer sein bzw. im Idealfall gar nicht stattfinden. Es wäre doch viel schöner wenn man einfach aus einer synchronen JS-Funktion mittels eines Einzeilers eine asnchrone Variante bauen könnte, in etwa so:

var asyncFn = macheAsynchron(synchroneFn);
asyncFn(arg1, arg2, function(result){
  console.log(result);
});

Meine kleine Library namens Unsync macht genau das. Und obwohl ein ganzer Haufen HTML5-Technik zum Einsatz kommt, ist die Funktionsweise eigentlich ganz simpel und die Browserunterstützung ist auch nicht vollends katastrophal.

Wie Unsync funktioniert

Einen neuen Web Worker startet man, indem man eine URL zu einem Script in den Worker-Constructor steckt:

var myWorker = new Worker('pfad/zu/worker.js');

Es steht aber nirgends geschrieben, dass ein Script für einen Worker tatsächlich in einer eigenen Datei existieren muss … die Spezifikationen sehen nur vor, dass die Constructorfunktion eine resource identified by url übergeben bekommen soll. Und Dateiressourcen nebst URL kann man in HTML5 einfach aus heißer Luft erzeugen, File API sei Dank. Der Blob-Constructor baut uns eine ausreichend dateiartige Ressource …

var myBlob = new Blob(['Hallo Welt!'], {
  type: 'text/plain'
});

… und die Funktion window.URL.createObjectURL() erzeugt eine URL auf diese Ressource, obwohl sie eigentlich nur ein JavaScript-Objekt in unserer JS-Sandbox ist:

var myBlobUrl = window.URL.createObjectURL(myBlob);
location.href = myBlobUrl;

Hiermit schickt uns der Browser an eine URL, die in etwa wie blob:http%3A//localhost/8f0a8135-ebef-43eb-b660-9ef69ce8cf3e aussieht und eine Plaintext-Datei mit dem Inhalt „Hallo Welt“ zu referenzieren scheint. Dass das Ganze nur innerhalb unserer JavaScript-Welt existiert, ist dabei für den Browser nicht von Belang – eine mit einer URL referenzierte Ressource ist eine mit einer URL referenzierte Ressource, egal woher. Und das klappt natürlich auch mit JavaScript:

var workerBlob = new Blob(['this.onmessage = function(){\
  for(var i = 0; i   5000000000; i++){\
    Math.sqrt(Math.random() * 1000000000);\
  }\
  this.postMessage(null);\
};'], {
  type: 'text/javascript'
});
var workerUrl = window.URL.createObjectURL(workerBlob);
var myWorker = new Worker(workerUrl);
myWorker.postMessage(null);
myWorker.onmessage = function(){
  window.alert('Fertig!')
};

… aber Code als String? Das geht ja nun mal gar nicht, schon gar nicht wenn es das performancekritische Herz unserer App ist. Lieber würde man eine Funktion in den Worker schieben. Das ist aber nicht ohne weiteres möglich – der für die Datenübermittlung in den Worker verwendete structured clone algorithm kann das nicht. Aber es gibt zum Glück Function.prototype.toString().

Funktionen haben, anders als die meisten anderen JavaScript-Objekte, eine sehr nützliche Variante der toString()-Methode. Diese spuckt tatsächlich eine brauchbare String-Repräsentation der betroffenen Funktion aus:

(function foo(){ return 42; }).toString()
// > "function foo(){ return 42; }"

Warum also nicht eine normale Funktion schreiben, sie stringifizieren und das ganze als Blob bzw. Objekt-URL in einen Web Worker schieben? Anfang (function berechnung(){) und Ende (}) der Funktion stehen hierbei freilich ein wenig im Weg, denn eigentlich wollen wir bloß den Inhalt der Funktion haben, nicht die gesamte Deklaration. Zum Glück können wir die Deklaration bequem in einen sofort ausgeführten Funktionsausdruck (IIFE) verwandeln. Hierfür müssen wir bloß ein paar Klammern vorn und hinten an den Funktionscode anbringen, was leicht ist, da der Blob-Constructor ohnehin als erstes Argument ein Array von Strings erwartet. Wir nehmen also die String-Repräsentation unserer Rechnen-Funktion, hängen vorn eine und hinten drei Klammern an und stecken das ganze via Blob und Blob-URL in den Worker:

// Quellfunktion
function berechnung(){
  this.onmessage = function(){
    for(var i = 0; i < 5000000000; i++){
      Math.sqrt(Math.random() * 1000000000);
    }
    this.postMessage(null);
  }
}

// Quellfunktion als String
var code = berechnung.toString();

// Blob aus dem String plus Klammern für die IIFE
var workerBlob = new Blob(['(', code, ')()'], {
  type: 'text/javascript'
});
var workerUrl = window.URL.createObjectURL(workerBlob);

// Worker erzeugen und benutzen
var myWorker = new Worker(workerUrl);
myWorker.postMessage(null);
myWorker.onmessage = function(){
  window.alert('Fertig!');
};

Die Quellfunktion berechnung() unterliegt einigen Beschränkungen. Es wird nicht die Funktion selbst, sondern nur ein diese Funktion abbildender String in den Web Worker geschoben, was bedeutet, dass die Funktion berechnung() selbst keine Seiteneffekte/Nebenwirkungen haben kann. Sie kann zum Beispiel nicht das DOM anfassen und kann keine Variablen aus übergeordneten Scopes verwenden. Außerdem muss sie zum jetzigen Zeitpunkt noch immer für Web Workers geschrieben werden, d.h. postMessage() zur Kommunikation über die Worker-Grenzen hinweg benutzen. Und das ist doof, denn der der Autor der Quellfunktion sollte nicht wissen müssen, wie ein Worker bzw. dessen Messaging-System funktioniert. Außerdem gibt es bisher keine Möglichkeit, Daten in den Worker hinein oder aus dem Worker herauszubekommen und wenn man den Worker nach getaner Arbeit schließen und seine Blob-URL deaktivieren könnte, wäre das ebenfalls nicht verkehrt. Und ohnehin wäre das ganze als Library-Funktion außerhalb des eigentlichen Moduls viel besser aufgeboben.

Die ersten beiden Hürden sind schnell genommen; nur der String, in den die Quellfunktion eingepackt wird, muss ein bisschen angepasst werden. Die Kommunikation zwischen Web Worker und Außenwelt wird über Events abgewickelt. Worker und Außenwelt verwenden die postMessage()-Methode, um auf der jeweiligen Gegenseite ein message-Event zu triggern. Das an postMessage() übergebene Argument wird als Nachricht übertragen, was mit so gut wie jedem JS-Datentyp funktioniert (Funktionen ausgenommen). Das wegzuabstrahieren ist recht simpel: wir ersetzen einfach die Klammern, die aus dem Funktionstemplate eine IIFE machen durch Code, der die Funktion in ein message-Event einpackt, die Arguments an das Funktionstemplate weiterleitet und das Ergebnis der Funktion mittels postMessage() zurücksendet. Aus alt …

var workerBlob = new Blob(['(', code, ')()'], {
  type: 'text/javascript'
});

 … wird neu:

var workerBlob = new Blob([
  'this.onmessage = function(evt){',
  '  var result = (', code, ').apply(null, evt.data);',
  '  this.postMessage(result);',
  '};'
], {
  type: 'text/javascript'
});

Jetzt kann als Quellfunktion wirklich jede Funktion herhalten, die ohne Seiteneffekte bzw. Nebenwirkungen und globale Variablen auskommt und der Autor der Funktion braucht nichts mehr über Web Workers zu wissen – dass es keine Seiteneffekte geben darf, sind die einzige verbleibende Beschränkung. Unsere Funktion berechnung() darf nun wieder aussehen wie eine ganz normale Funktion, ist schön zu lesen und durch die Verwendung im Web Worker aus nicht mehr blockierend:

// Quellfunktion
function berechnung(){
  for(var i = 0; i < 500000000; i++){
    Math.sqrt(Math.random() * 1000000000);
  }
}

// Quellfunktion als String
var code = berechnung.toString();

// Der Rest geht von selbst :)

Nun müsste nur noch der übrige Web-Worker-spezifische Code aus unserem Modul verschwinden. Außerdem gibt es noch zwei Details, die beachtet werden wollen und die sich nicht komplett abstrahieren lassen. Mit createObjectURL() erzeugte URLs sind potenziell immerwährende Referenzen auf das betroffene Objekt und daher ein schönes Speicherleck, was auch für die Web Workers selbst gilt – wenn wir die Hintergrundprozesse nicht terminieren, tut es niemand und sie verbrauchen bis ans Ende aller Tage Speicherplatz und Rechenpower. Die Unsyc-Library versucht all das so bequem wie möglich zu gestalten.

Unsync benutzen

Unsync besteht aus eigentlich nur einer einzigen Funktion namens unsync(), die aus einer synchronen Funktion ohne Seiteneffekte/Nebenwirkungen eine äquivalente asynchrone Funktion erzeugt:

var asyncFn = unsync(fn);
asyncFn(callback);

Wenn die originale Funktion n Arguments erwartet, erwartet die asynchrone Funktion n + 1 – alle normalen Arguments plus einem Callback am Ende. Dem Callback wird als erstes Argument das von der Quellfunktion errechnete Ergebnis übergeben:

// Quellfunktion; rechnet rum und gibt ein Benchmark zurück
function crunchNumbers(x){
  var startTime = new Date();
  for(var i = 0; i < x; i++){
    Math.sqrt(Math.random() * 1000000000);
  }
  var totalTime = new Date() - startTime;
  return totalTime;
}

// Asynchrone Version der obrigen Funktion erzeugen
var crunchAsync = unsync(crunchNumbers);

// Async-Function mit Arguments und Callback ausrufen
crunchAsync(5000000000, function(time){
  window.alert('Fertig nach ' + time);
});

So muss man fast gar nichts mehr über Web Workers wissen – fast. Das einzige, was sich nicht komplett automatsieren lässt, ist das Abschalten der Worker-Hintergrundprozesse, wenn diese nicht mehr gebraucht werden. Hierfür gibt es in Unsync zwei Möglichkeiten. Zum einem kann eine Einweg-Funktion erzeugt werden, die ihren Hintergrundprozess nach der erstmaligen Verwendung selbsttätig terminiert. Diese Funktion kann dann nur exakt einmal verwendet werden und reagiert auf neuerliche Aufruf-Versuche mit Exceptions. Um diesen Selbstzerstörungs-Mechanismus zu aktivieren einfach unsync() als zweites Argument true mitgeben:

var unsyncedEinweg = unsync(fn, true);
unsyncedEinweg(function(){      // Einmal klappt...
  unsyncedEinweg(function(){}); // Exception!
});

Alternativ können mit unsync() erzeugte Funktionen über ihre terminate()-Methode manuell ihren Worker ausgeknipst bekommen. Die Eigenschaft isTerminated verrät, ob der Worker einer Funktion terminiert wurde:

var unsynced = unsync(fn, true);
unsynced(); // Berechnung startet

// Wenn wir nach einer Minute nicht fertig sind und terminiert
// haben, ist es eh zu spät...
setTimeout(function(){
  if(!unsynced.isTerminated){
    unsynced.terminate();
    window.alert('Timeout!');
  }
}, 60 * 1000);

Das wäre eigentlich alles, was es zur Benutzung Unsync zu wissen gibt, wären da nicht die Browser bzw. die zwei ausgesuchten Prachtexemplare ihrer Zunft.

Wo ist der Haken?

Alle halbwegs aktuellen Versionen aller relevanten Browser unterstützen die nötigen APIs … bis auf den Android-Browser, der keine Web Workers kennt. Die IE 10 und 11 unterstützen zwar alle APIs, wissen aber anscheinend nicht, dass Blob-URLs dem gleichen Origin wie die sie erzeugende Webseite zuzuordnen sind. Für den IE gelten alle Blob-URLs gefährlicher Schadcode von außerhalb, der nicht ausgeführt werden darf. Das macht die komplette Objekt-URL-API im IE ziemlich nutzlos und verhindert natürlich auch, dass Unsync dort funktioniert. Es gibt mehrere Bug Reports zu dem Thema (das ist meiner, falls ihr ein paar me too-Kommentare loswerden wollt), aber Microsoft macht bisher nicht den Eindruck, als wäre ein Fix für den finalen IE 11 angedacht. Da dürfen wir wohl auf den IE 12 warten. Bis dahin gilt:

Browser Chrome Firefox Safari Opera iOS Android IE
Unterstützung

Bis dahin könnt ihr Unsync eigentlich in der Pfeife rauchen und dürft weiterhin mit Extra-Dateien für Web Workers jonglieren. Aber immerhin besteht zumindest für eure Enkel Hoffnung auf bequeme asynchrone Funktionen im IE 18 … und das ist doch auch schon mal was, oder?

Folgt mir!

Kauft mein Zeug!

Cover der HTML5-DVD
Infos auf html5-dvd.de, kaufen bei Amazon

Cover des HTML5-Buchs
Infos auf html5-buch.de, kaufen bei Amazon

Cover des CSS3-Buchs
Infos auf css3-buch.de, kaufen bei Amazon