Einführung in die asynchrone JavaScript-Programmierung
Seite 4: Generatorfunktionen, async und await
Generatorfunktionen zur asynchronen Programmierung
Betrachtet man die Zeile
let shallContinue = yield candidate;
isoliert, fällt auf, dass das Zurückgeben der Variablen candidate und das Entgegennehmen des Rückgabewerts zeitlich getrennt stattfinden: Der externe Aufruf von next bestimmt, wie viel Zeit dazwischen vergeht. Letztlich entspricht das also dem Pausieren einer Funktion, wobei sich während der Wartezeit anderer Code ausführen lässt.
Wenn das gleiche Vorgehen auf asynchronen Code anwendbar wäre, ließe sich ein asynchroner Aufruf wie folgt schreiben:
let data = yield fs.readFile('/etc/passwd');
Die Möglichkeit würde die Lesbarkeit von asynchronem Code deutlich verbessern, da der einzige Unterschied zwischen einem asynchronen und einem synchronen Aufruf in der Verwendung des Schlüsselworts yield bestünde.
Allerdings müsste die Funktion fs.readFile dann so geschrieben sein, dass sie keinen Callback erwartet, sondern ein Objekt synchron zurückgibt, auf das an anderer Stelle gewartet und reagiert werden kann. Genau das ermöglichen Promises:
let fsReadFile = promisify(fs, 'readFile');
let data = yield fsReadFile('/etc/passwd');
Das Beispiel funktioniert dennoch nicht, da noch eine Ablaufsteuerung fehlt, die auf das Promise reagiert und intern next aufruft. Das leistet das Modul co.
ES7: async und await
Der Einsatz von co erübrigt sich allerdings in absehbare Zeit, da die nächste Version von JavaScript, ES7, eingebaute Unterstützung für das Vorgehen mit den Schlüsselwörtern async und await enthält. Das Schlüsselwort async tritt dann an die Stelle der Generatorfunktionen, await ersetzt yield.
Wäre ES7 bereits heute verfügbar, ließe sich die load-Funktion wie folgt schreiben:
let fsReadFile = promisify(fs, 'readFile');
let load = async function (filename) {
load.cache = load.cache || {};
let data = load.cache[filename];
if (data) {
return data.toString('utf8');
}
data = await fsReadFile(filename);
load.cache[filename] = data;
return data.toString('utf8');
};
Das entspricht mit Ausnahme der beiden neuen Schlüsselwörter exakt dem synchronen Code. Auf die Weise verbessert sich nicht nur die Lesbarkeit deutlich, sondern Entwickler können auch der Callback Hell entgehen.
Außerdem ist es nicht mehr möglich, Fehler versehentlich zu verschlucken, da async und await dafür Sorge tragen, dass im Fall eines zurückgewiesenen Promises eine Ausnahme geworfen wird, die mit try und catch abzufangen ist. Zu guter letzt lassen sich auch die übrigen Konstrukte zur Ablaufsteuerung verwenden, beispielsweise for-Schleifen.
Der einzige Haken ist, dass sich await ausschließlich in Funktionen verwenden lässt, die als async markiert sind. Das bedeutet, dass auf oberster Ebene eine async-Funktion stehen muss. Das lässt sich jedoch einfach bewerkstelligen, indem ein asynchroner Lambda-Ausdruck als "Main"-Funktion zum Einsatz kommt, der automatisch ausgeführt wird:
(async () => {
let data = await load('/etc/passwd');
console.log(data);
})();
Obwohl sich der Code wie synchroner Code liest, verhält er sich asynchron: Node.js blockiert nicht, während es wartet, dass die Datei fertig geladen ist. Unter der Haube arbeitet er nach wie vor mit Promises und Callbacks, für Entwickler verbirgt die Syntax das jedoch auf elegante Weise.
Das bedeutet allerdings, dass eine nicht behandelte Ausnahme abzufangen ist, da auch sie asynchron auftritt. Daher ist es ratsam, auf oberster Ebene ein globales try zu verwenden:
(async () => {
try {
let data = await load('/etc/passwd');
console.log(data);
} catch (err) {
console.error(err);
}
})();
Alternativ lässt sich auf das Ereignis process.unhandledRejection reagieren:
process.on('unhandledRejection', (reason, p) => {
console.error(`Unhandled Rejection at: Promise ${p}, reason: ${reason}`);
});
(async () => {
let data = await load('/etc/passwd');
console.log(data);
})();
Besonders interessant ist die Fähigkeit von await, auf mehrere asynchrone Funktionen gleichzeitig warten zu können, die parallel ausgeführt werden. Dazu dient das alternative Schlüsselwort await*, das im zugehörigen Proposal beschrieben ist.
Auch wenn die neuen Schlüsselwörter in ES7 noch nicht final sind und ES7 davon abgesehen auch noch nicht auf breiter Front verfügbar ist, lässt sich die neue Syntax dennoch verwenden. Das Projekt Babel ermöglicht das, indem es einen Compiler anbietet, der zukünftig lauffähigen ES2015- und ES7-Code in heute ausführbaren ES5-Code übersetzt.
Im einfachsten Fall installiert man Babel global via npm:
npm install -g babel
Dabei verwendet der Compiler die lokal vorhandene Version von Node.js als Ausführungsumgebung. Da die Sprachmerkmale von ES7 noch als experimentell eingestuft sind, ist die Unterstützung für sie beim Aufruf von Babel explizit zu aktivieren:
babel-node --optional es7.asyncFunctions app.js
Alternativ lässt sich Babel auch auf anderen Wegen installieren. Die Dokumentation beschreibt die unterschiedlichen Vorgehensweisen.