Über mich | Kontakt | Archiv

Thomas Bandt

Dieses Blog wird nicht mehr gepflegt. Zur neuen Website!

Vielversprechend: Promises in JavaScript

Wer sich eine Weile mit JavaScript beschäftigt, wird bei der Verwendung einer Library oder eines Frameworks über kurz oder lang Code-Fragmenten begegnen, die einen starken Hang dazu aufweisen, nach rechts zu wachsen. Willkommen in der Callback-Hölle.

Die Ursache liegt darin, dass in JavaScript seit jeher versucht wird, asynchron zu arbeiten, da zur Ausführung nur ein Thread zur Verfügung steht.

Kommuniziert man also z.B. aus einer Website heraus mit einem Server, so will man ja nicht, dass die komplette Seite im aktuellen Browserfenster bzw. -Tab lahmgelegt ist, solange die Antwort vom Server noch aussteht. Gleiches gilt für den Zugriff auf das Dateisystem in Cordova oder die Verbindung zu einer Datenbank in NodeJS.

Das kann sich dann in seiner hässlichsten Schreibweise z.B. so auswachsen:

(function(homeController) {
	var mongodb = require("mongodb");
	var mongoUrl = "mongodb://localhost:27017/demodb";
	
	homeController.init = function(app) {
	    app.get("/", function(request, response) {
	        mongodb.MongoClient.connect(mongoUrl, function(error, db) {
	            db.users.find().toArray(function(error, result) {
	                response.render("index", {
	                    error: error,
	                    result: result
	                });
	            });
	        });
	    });
	};
}(module.exports));

Hier werden alle User abgerufen und zurück zum Client gegeben. Das kann man natürlich noch ein wenig angenehmer schreiben, in dem man die Callbacks nicht inline definiert, sondern als eigene Funktionen rauslegt, was sich ein wenig besser liest:

(function(homeController) {
	var mongodb = require("mongodb");
	var mongoUrl = "mongodb://localhost:27017/demodb";
	
	var _response;
	
	homeController.init = function(app) {
	    app.get("/", handleRequest);
	};
	
	function handleRequest(request, response) {
	    _response = response;
	
	    mongodb.MongoClient.connect(mongoUrl, connectedToMongoDB);
	}
	
	function connectedToMongoDB(error, db) {
	    db.users.find().toArray(mapUsers);
	}
	
	function mapUsers(error, result) {
	    _response.render("index", {
	        error: error,
	        result: result
	    });
	}
}(module.exports));

Aber für jeden einzelnen Schritt eine eigene Funktion – auch das zieht sich sehr schnell in die Länge und lässt einen irgendwann orientierungslos im eigenen Code herumspringen.

Eine Alternative hierzu ist es, Promises einzusetzen. Eine beliebte Library hierfür wäre z.B. q.

(function(homeController) {
	var mongodb = require("mongodb");
	var mongoUrl = "mongodb://localhost:27017/demodb";
	
	var $q = require("q");
	
	homeController.init = function(app) {
	    app.get("/", function(request, response) {
	        getDemoData().then(function(result) {
	            response.render("index", {
	                users: result
	            });
	        }, function(error) {
	            response.render("index", {
	                error: error,
	                users: []
	            });
	        })
	    });
	};
	
	function getDemoData()) {
	    return getDb().then(function(db) {
	        var deferred = $q.defer();
	
	        db.users.find().toArray(function(error, results) {
	            if (error) {
	                deferred.reject(error);
	            } else {
	                deferred.resolve(results);
	            }
	        });
	
	        return deferred.promise;
	    })
	}
	
	function getDb() {
	    var deferred = $q.defer();
	
	    mongodb.MongoClient.connect(mongoUrl, function(error, db) {
	        if (error) {
	            deferred.reject(error);
	        } else {
	            deferred.resolve(db);
	        }
	    });
	
	    return deferred.promise;
	};
}(module.exports));

Was sich vielleicht auf den ersten Blick noch nicht erschließt, macht sich gerade bei komplexeren Szenarien positiv bemerkbar:

Bei den eigenen Funktionen kann man auf Callbacks verzichten, sie kommen nur noch bei den externen Komponenten zum Einsatz, deren API man selbst nicht ändern kann (in dem Fall der Datenbankprovider für MongoDB in NodeJS).

Stattdessen gibt jede Funktion etwas Erwartbares zurück: nämlich das Versprechen, sich nach erfolgreicher oder fehlgeschlagener Ausführung mit dem Ergebnis zurückzumelden.

Das funktioniert, grob gesagt, in dem innerhalb der Funktion ein Objekt erzeugt und direkt an den Aufrufer der Funktion zurückgegeben wird, das quasi einen Vertrag besiegelt - indem es “verspricht”, sich zu melden, sobald die Arbeit innerhalb der Funktion erledigt ist.

Wie lange es auch dauert, was da dann intern passiert - sobald alles getan ist, kann das Versprechen über resolve([Ergebnis]) oder reject([Fehler]) eingelöst werden.

Darauf reagiert dann der Aufrufer - denn der hat zu diesem Zeitpunkt über die then()-Funktion bereits festgelegt, was im Erfolgs- und was im Fehlerfall passieren soll.

Noch mal an einem simpleren Beispiel:

asyncFoo().then(success, failure);

function asyncFoo() {
	var deferred = $q.defer();

	setTimeout(function() {
		deferred.resolve("Fertig");
	}, 1000);

	return deferred;
}

function success(result) {
	console.log("Yeah: " + result);
}

function failure(error) {
	console.warn("Oops: " + error)
}

Nach genau 1.000 Millisekunden wird hier “Yeah: Fertig” ausgegeben.

Das ist nur ein sehr kleiner Einblick in die Funktionsweise von Promises - aber der wie ich finde wichtigste Part. Die Einarbeitung lohnt sich. async await in C# ist zwar durchaus eleganter, aber wenn man das Konzept einmal verinnerlicht hat, lässt sich auch in JavaScript deutlich lesbarerer Code schreiben.

Kommentare

  1. Marcell Spies schrieb am Dienstag, 18. November 2014 01:39:00 Uhr:

    Hi Thomas,

    in ES6 wird es Generator Functions geben (bei NodeJS aktivierbar mit --harmony Flag), mit denen du den Code noch sauberer hinbekommst.

    http://blog.alexmaccaw.com/how-yield-will-transform-node

    In ES7 (okay, dauert noch etwas) gibt's aktuell zumindest schon einen Vorschlag für eine async/await Syntax. Im Traceur-Transpiler wird die Syntax schon experimentell unterstützt.

    https://github.com/google/traceur-compiler/wiki/LanguageFeatures#async-functions-experimental

    Viele Grüße
    Marcell
  2. Thomas schrieb am Dienstag, 18. November 2014 08:56:00 Uhr:

    Hi,

    danke für die Links. Schön erklärt in dem Blogpost.

    Die Generator-Functions sind schon mal ein großer Fortschritt, hatte dazu schon im Zusammenhang mit NodeJS und Koa gelesen. Rein von der Lesbarkeit ist das natürlich trotzdem noch ausbaufähig, da geht Google mit Traceur in die richtige Richtung. Microsoft hat mit async/await doch einen richtig guten Job gemacht ;-).

    Thomas


« Zurück  |  Weiter »