node.js machte hochgradig nebenläufige Server einfach, indem alles über nicht-blockierende E/A lief, und der Preis zeigte sich in der Struktur: Jeder Datenbankzugriff oder API-Aufruf fügte eine Callback-Ebene hinzu, und koushs eigene Projekte, die darauf ausgelegt waren, Zehntausende lang laufender Verbindungen zu halten, liefen routinemäßig sieben oder acht Einrückungsebenen tief. Ein einfacher Request plus Query benötigte bereits zwei verschachtelte Callbacks, die Fehlerbehandlung umschloss jede Ebene mit ihrem eigenen try/catch, und serialisierte Arbeit wie paginierte API-Aufrufe verwandelte sich in rekursive Fortsetzungsfunktionen.
Die 2011 verfügbaren Lösungen verschoben das Problem, statt es zu beseitigen: Der yield-Operator unterbrach und setzte Iteratoren fort, blieb aber unhandlich, und Source-to-Source-Übersetzer wie TameJS kompilierten sauber aussehenden Code in gewöhnliches JavaScript, das überall laufen konnte – zum Preis, dass man den generierten Code statt den vom Entwickler geschriebenen Code debuggen musste. Koushs Bezugspunkt war async/await aus C# 5, ein explizites Sprachmerkmal, das asynchronen Code synchron lesbar machte.
Sein Schritt war, dieses Merkmal in die Laufzeitumgebung einzubauen: ein Fork von node.js mit in V8 implementierten Koroutinen, einem async-Funktionsmarker und einem await, das die Funktion beendet und sie wieder aufnimmt, wenn der Callback abgeschlossen ist. Der Parser erzeugte weiterhin ungefähr denselben AST wie die Callback-Version, sodass ein try/catch jeden await überspannte und eine while-Schleife paginierte API-Anfragen serialisieren konnte, während ndb und node-inspector weiterhin den Originalcode debuggen konnten. Der Preis war ein eigener Node-Build, den ein Team, das seine eigenen Server kontrolliert, realistischerweise zahlen konnte.
Die Funktion bei await über V8-Koroutinen aussetzen, sodass asynchroner Code sich wie synchroner Code liest
Verschachtelte Callbacks vervielfachen sich mit jedem E/A-Schritt und drängen die Fehlerbehandlung in getrennte Gültigkeitsbereiche; await hält ein einziges try/catch um einen gesamten Ablauf, sodass Ausnahmen Callback-Grenzen wie synchroner Code überschreiten.
Echte Koroutinen in der Laufzeitumgebung statt Source-to-Source-Übersetzung sorgen dafür, dass der vom Entwickler geschriebene Code auch der ausgeführte Code bleibt, sodass ndb und node-inspector das Original statt der generierten Ausgabe debuggen.
Sequenziell wirkender Kontrollfluss lässt gewöhnliche Schleifen serialisierte asynchrone Arbeit ausdrücken: Das paginierte GDATA-Beispiel ersetzt eine rekursive Chunk-Funktion durch eine while-Schleife.
Da ein Serverteam seine Bereitstellung kontrolliert, war das Forken von node.js und V8 ein realistischer Preis, und das Design wurde als funktionierender Branch mit Tests ausgeliefert statt als Vorschlag.
Wenn Workarounds Komplexität in generierten Code oder tiefere Verschachtelung drängen, die Sprachebene reparieren: die gesamte Funktion bei E/A aussetzen, sodass lesbarer Code das ist, was läuft und was man debuggt
Das Projekt blieb ein Fork statt eines zusammengeführten Features: Das Wiki, zuletzt im April 2013 nach 25 Revisionen bearbeitet, rahmte einen eigenen Node-Build als Bereitstellungsoption ein, und seine TODO-Liste nannte die Optimierung von Branches, die await enthielten. Am 29. September 2011 auf Hacker News gepostet, erhielt es 222 Punkte und 101 Kommentare; Leser nannten den Ansatz sehr elegant und stimmten zu, dass die richtige Lösung darin bestehe, Koroutinen einzuführen. Nichts in der Aufzeichnung zeigt, dass der Patch in node.js Eingang fand; seine sichtbare Wirkung war, mit funktionierendem Code zu demonstrieren, wie JavaScript async/await auf Sprachebene erhalten könnte.
DEN BELEGEN FOLGEN
Die Quellen
- "async" support in node.js (koush/node wiki) GitHub — koush/node wiki · github.com
- Fixing the callback spaghetti in node.js | Hacker News Hacker News · news.ycombinator.com ·