Nach vielen Änderungen hat sich diese Antwort in der Länge zu einem Monster entwickelt. Ich entschuldige mich im Voraus.
Erstens eval()
ist es nicht immer schlecht und kann sich positiv auf die Leistung auswirken, wenn es zum Beispiel in der Lazy-Evaluation eingesetzt wird. Lazy-Evaluation ähnelt dem Lazy-Loading, aber Sie speichern Ihren Code im Wesentlichen in Strings und verwenden eval
oder new Function
, um den Code zu evaluieren. Wenn Sie einige Tricks anwenden, wird es viel nützlicher als das Böse, aber wenn Sie es nicht tun, kann es zu schlechten Dingen führen. Sie können sich mein Modulsystem ansehen, das dieses Muster verwendet: https://github.com/TheHydroImpulse/resolve.js . Resolve.js verwendet eval, anstatt in new Function
erster Linie das CommonJS exports
und die module
Variablen zu modellieren, die in jedem Modul verfügbar sind, und new Function
umschließt Ihren Code mit einer anonymen Funktion. Am Ende verpacke ich jedoch jedes Modul in eine Funktion, die ich manuell in Kombination mit eval durchführe.
In den folgenden beiden Artikeln lesen Sie mehr darüber, wobei sich der spätere auch auf den ersten bezieht.
Harmony-Generatoren
Jetzt, da Generatoren endlich in V8 und damit in Node.js gelandet sind, unter einer Flagge ( --harmony
oder --harmony-generators
). Dies reduziert die Anzahl der Rückrufe erheblich. Es macht das Schreiben von asynchronem Code wirklich großartig.
Der beste Weg, um Generatoren zu nutzen, besteht darin, eine Art Kontrollflussbibliothek zu verwenden. Dadurch kann der Fluss fortgesetzt werden, während Sie innerhalb der Generatoren nachgeben.
Rückblick / Übersicht:
Wenn Sie mit Generatoren nicht vertraut sind, unterbrechen Sie die Ausführung spezieller Funktionen (sogenannte Generatoren). Diese Praxis nennt man Nachgeben mit dem yield
Schlüsselwort.
Beispiel:
function* someGenerator() {
yield []; // Pause the function and pass an empty array.
}
Wenn Sie diese Funktion also zum ersten Mal aufrufen, wird eine neue Generatorinstanz zurückgegeben. Auf diese Weise können Sie next()
dieses Objekt aufrufen , um den Generator zu starten oder fortzusetzen.
var gen = someGenerator();
gen.next(); // { value: Array[0], done: false }
Sie würden so lange telefonieren, next
bis Sie done
zurückkehren true
. Dies bedeutet, dass der Generator seine Ausführung vollständig abgeschlossen hat und keine weiteren yield
Anweisungen vorliegen.
Kontrollfluss:
Wie Sie sehen, erfolgt die Steuerung von Generatoren nicht automatisch. Sie müssen jedes manuell fortsetzen. Deshalb werden Kontrollflussbibliotheken wie co verwendet.
Beispiel:
var co = require('co');
co(function*() {
yield query();
yield query2();
yield query3();
render();
});
Dies ermöglicht die Möglichkeit, alles in Node (und den Browser mit dem Regenerator von Facebook , der als Eingabe Quellcode verwendet, der Harmony-Generatoren verwendet und voll kompatiblen ES5-Code aufteilt) synchron zu schreiben.
Generatoren sind noch ziemlich neu und erfordern daher Node.js> = v11.2. Während ich dies schreibe, ist v0.11.x immer noch instabil und daher sind viele native Module defekt und werden es bis v0.12 sein, wo sich die native API beruhigen wird.
Zu meiner ursprünglichen Antwort hinzufügen:
Ich bevorzuge kürzlich eine funktionalere API in JavaScript. Die Konvention verwendet OOP im Hintergrund, wenn dies erforderlich ist, vereinfacht jedoch alles.
Nehmen wir zum Beispiel ein View-System (Client oder Server).
view('home.welcome');
Ist viel einfacher zu lesen oder zu befolgen als:
var views = {};
views['home.welcome'] = new View('home.welcome');
Die view
Funktion prüft einfach, ob dieselbe Ansicht bereits in einer lokalen Karte vorhanden ist. Wenn die Ansicht nicht vorhanden ist, wird eine neue Ansicht erstellt und der Karte ein neuer Eintrag hinzugefügt.
function view(name) {
if (!name) // Throw an error
if (view.views[name]) return view.views[name];
return view.views[name] = new View({
name: name
});
}
// Local Map
view.views = {};
Sehr einfach, oder? Ich finde, dass es die öffentliche Benutzeroberfläche dramatisch vereinfacht und die Verwendung erleichtert. Ich benutze auch Kettenfähigkeit ...
view('home.welcome')
.child('menus')
.child('auth')
Tower, ein Framework, das ich (mit jemand anderem) oder die nächste Version (0.5.0) entwickle, wird diesen funktionalen Ansatz in den meisten seiner offengelegten Schnittstellen verwenden.
Einige Leute nutzen Fasern, um eine "Rückruf-Hölle" zu vermeiden. Es ist eine ganz andere Herangehensweise an JavaScript, und ich bin kein großer Fan davon, aber viele Frameworks / Plattformen verwenden es. einschließlich Meteor, da sie Node.js als Thread / pro Verbindungsplattform behandeln.
Ich würde lieber eine abstrahierte Methode verwenden, um eine Rückruf-Hölle zu vermeiden. Es kann umständlich werden, aber es vereinfacht den eigentlichen Anwendungscode erheblich. Bei der Erstellung des TowerJS- Frameworks konnten viele unserer Probleme behoben werden. Sie haben jedoch offensichtlich immer noch einige Rückrufe, aber die Verschachtelung ist nicht tiefgreifend.
// app/config/server/routes.js
App.Router = Tower.Router.extend({
root: Tower.Route.extend({
route: '/',
enter: function(context, next) {
context.postsController.page(1).all(function(error, posts) {
context.bootstrapData = {posts: posts};
next();
});
},
action: function(context, next) {
context.response.render('index', context);
next();
},
postRoutes: App.PostRoutes
})
});
Ein Beispiel für unser derzeit entwickeltes Routing-System und unsere "Controller", die sich jedoch von den herkömmlichen "rail-like" unterscheiden. Aber das Beispiel ist extrem leistungsfähig und minimiert die Anzahl der Rückrufe und macht die Dinge ziemlich offensichtlich.
Das Problem bei diesem Ansatz ist, dass alles abstrahiert ist. Nichts läuft so wie es ist und erfordert ein "Framework" dahinter. Wenn diese Art von Funktionen und Codierungsstil jedoch in einem Framework implementiert wird, ist dies ein enormer Gewinn.
Bei Mustern in JavaScript kommt es ehrlich gesagt darauf an. Vererbung ist nur dann wirklich nützlich, wenn Sie CoffeeScript, Ember oder ein "Klassen" -Framework / eine "Klassen" -Infrastruktur verwenden. Wenn Sie sich in einer "reinen" JavaScript-Umgebung befinden, funktioniert die Verwendung der traditionellen Prototyp-Oberfläche wie ein Zauber:
function Controller() {
this.resource = get('resource');
}
Controller.prototype.index = function(req, res, next) {
next();
};
Zumindest für mich begann Ember.js mit einer anderen Herangehensweise beim Konstruieren von Objekten. Anstatt die einzelnen Prototypmethoden unabhängig voneinander zu erstellen, verwenden Sie eine modulartige Schnittstelle.
Ember.Controller.extend({
index: function() {
this.hello = 123;
},
constructor: function() {
console.log(123);
}
});
All dies sind verschiedene "Codierungs" -Stile, die jedoch zu Ihrer Codebasis beitragen.
Polymorphismus
Der Polymorphismus wird in reinem JavaScript nicht häufig verwendet, da für die Arbeit mit der Vererbung und das Kopieren des "klassen" -ähnlichen Modells viel Code erforderlich ist.
Ereignis- / komponentenbasiertes Design
Ereignis- und komponentenbasierte Modelle sind die Gewinner der IMO oder die am einfachsten zu bearbeitenden Modelle, insbesondere bei der Arbeit mit Node.js, das über eine integrierte EventEmitter-Komponente verfügt. Die Implementierung solcher Emitter ist jedoch trivial, eine nette Ergänzung .
event.on("update", function(){
this.component.ship.velocity = 0;
event.emit("change.ship.velocity");
});
Nur ein Beispiel, aber es ist ein schönes Modell, mit dem man arbeiten kann. Besonders in einem spiel- / komponentenorientierten Projekt.
Das Komponentendesign ist ein eigenständiges Konzept, aber ich denke, es funktioniert hervorragend in Kombination mit Ereignissystemen. Spiele sind traditionell für komponentenbasiertes Design bekannt, bei dem die objektorientierte Programmierung nur so weit führt.
Das komponentenbasierte Design hat seine Verwendung. Es hängt davon ab, welche Art von System Ihr Gebäude hat. Ich bin mir sicher, dass es mit Web-Apps funktionieren würde, aber es würde in einer Spielumgebung aufgrund der Anzahl von Objekten und separaten Systemen sehr gut funktionieren, aber es gibt sicherlich andere Beispiele.
Pub / Sub-Muster
Event-Binding und Pub / Sub sind ähnlich. Das Pub- / Sub-Muster glänzt in Node.js-Anwendungen aufgrund der einheitlichen Sprache, kann jedoch in jeder Sprache verwendet werden. Funktioniert hervorragend in Echtzeitanwendungen, Spielen usw.
model.subscribe("message", function(event){
console.log(event.params.message);
});
model.publish("message", {message: "Hello, World"});
Beobachter
Dies kann subjektiv sein, da einige Leute das Observer-Muster als Pub / Sub betrachten, aber sie haben ihre Unterschiede.
"Der Beobachter ist ein Entwurfsmuster, bei dem ein Objekt (bekannt als Subjekt) eine Liste von Objekten verwaltet, die von ihm abhängen (Beobachter), und diese automatisch über Statusänderungen benachrichtigt." - Das Beobachtermuster
Das Beobachtermuster ist ein Schritt über typische Pub / Sub-Systeme hinaus. Objekte haben strenge Beziehungen oder Kommunikationsmethoden miteinander. Ein Objekt "Betreff" würde eine Liste von abhängigen "Beobachtern" führen. Das Thema würde seine Beobachter auf dem Laufenden halten.
Reaktive Programmierung
Reaktive Programmierung ist ein kleineres, unbekannteres Konzept, insbesondere in JavaScript. Es gibt ein Framework / eine Bibliothek (die ich kenne), die eine einfach zu handhabende API zur Verwendung dieser "reaktiven Programmierung" bereitstellt.
Ressourcen zur reaktiven Programmierung:
Im Grunde handelt es sich um eine Reihe von Synchronisierungsdaten (seien es Variablen, Funktionen usw.).
var a = 1;
var b = 2;
var c = a + b;
a = 2;
console.log(c); // should output 4
Ich glaube, dass reaktive Programmierung, insbesondere in imperativen Sprachen, stark verborgen ist. Es ist ein erstaunlich leistungsfähiges Programmierparadigma, insbesondere in Node.js. Meteor hat eine eigene reaktive Engine entwickelt, auf der das Framework basiert. Wie funktioniert Meteors Reaktivität hinter den Kulissen? gibt einen guten Überblick darüber, wie es intern funktioniert.
Meteor.autosubscribe(function() {
console.log("Hello " + Session.get("name"));
});
Dies wird normal ausgeführt und zeigt den Wert von an name
, aber wenn wir ihn ändern
Session.set ('name', 'Bob');
Das angezeigte console.log wird erneut ausgegeben Hello Bob
. Ein einfaches Beispiel, aber Sie können diese Technik auf Echtzeitdatenmodelle und -transaktionen anwenden. Sie können extrem leistungsfähige Systeme hinter diesem Protokoll erstellen.
Meteor ...
Reaktivmuster und Beobachtermuster sind sehr ähnlich. Der Hauptunterschied besteht darin, dass das Beobachtermuster häufig den Datenfluss mit ganzen Objekten / Klassen beschreibt, während reaktive Programmierung den Datenfluss zu bestimmten Eigenschaften beschreibt.
Meteor ist ein großartiges Beispiel für reaktive Programmierung. Die Laufzeit ist etwas kompliziert, da JavaScript keine nativen Werteänderungsereignisse aufweist (dies wird durch Harmony-Proxys geändert). Andere clientseitige Frameworks, Ember.js und AngularJS, verwenden ebenfalls reaktive Programmierung (in gewissem Umfang).
Die beiden späteren Frameworks verwenden das reaktive Muster vor allem in ihren Vorlagen (dh automatische Aktualisierung). Angular.js verwendet eine einfache schmutzige Prüftechnik. Ich würde das nicht als genau reaktive Programmierung bezeichnen, aber es ist nah, da Dirty Checking nicht in Echtzeit erfolgt. Ember.js verwendet einen anderen Ansatz. Verwendung set()
und get()
Methoden von Ember, mit denen sie abhängige Werte sofort aktualisieren können. Mit ihrer Runloop ist sie äußerst effizient und ermöglicht abhängigere Werte, bei denen der Winkel eine theoretische Grenze hat.
Versprechen
Keine Behebung von Rückrufen, sondern Herausnehmen von Einrückungen und Minimieren der verschachtelten Funktionen. Es fügt dem Problem auch eine nette Syntax hinzu.
fs.open("fs-promise.js", process.O_RDONLY).then(function(fd){
return fs.read(fd, 4096);
}).then(function(args){
util.puts(args[0]); // print the contents of the file
});
Sie können die Rückruffunktionen auch so verteilen, dass sie nicht inline sind, aber das ist eine andere Entwurfsentscheidung.
Ein anderer Ansatz wäre, Ereignisse und Versprechungen dahingehend zu kombinieren, dass Sie die Funktion haben, Ereignisse entsprechend zu senden, und dann die tatsächlichen Funktionsfunktionen (die die eigentliche Logik enthalten) an ein bestimmtes Ereignis zu binden. Sie würden dann die Dispatcher-Methode in jeder Callback-Position übergeben. Allerdings müssten Sie einige Probleme herausfinden, die Ihnen in den Sinn kämen, z. B. Parameter, das Wissen, an welche Funktion Sie senden sollen usw.
Einzelfunktion Funktion
Anstatt ein riesiges Durcheinander von Rückrufen zu haben, behalten Sie eine einzelne Funktion für eine einzelne Aufgabe bei und erledigen Sie diese Aufgabe gut. Manchmal können Sie sich selbst übertreffen und jeder Funktion mehr Funktionen hinzufügen. Fragen Sie sich jedoch: Kann dies eine eigenständige Funktion werden? Nennen Sie die Funktion, und dies bereinigt Ihre Einrückung und als Ergebnis bereinigt das Problem der Rückrufhölle.
Am Ende würde ich vorschlagen, ein kleines "Framework" zu entwickeln oder zu verwenden, das im Grunde nur ein Rückgrat für Ihre Anwendung ist, und mir Zeit nehmen, um Abstraktionen vorzunehmen, ein ereignisbasiertes System zu wählen oder "viele kleine Module zu verwenden, die es sind" unabhängiges "System. Ich habe mit mehreren Node.js-Projekten gearbeitet, bei denen der Code vor allem wegen der Rückruf-Hölle extrem chaotisch war, aber auch mangelnde Überlegungen, bevor sie mit dem Codieren begannen. Nehmen Sie sich Zeit, um die verschiedenen Möglichkeiten in Bezug auf API und Syntax zu durchdenken.
Ben Nadel hat einige wirklich gute Blog-Posts über JavaScript und einige ziemlich strenge und fortgeschrittene Muster verfasst, die in Ihrer Situation funktionieren können. Einige gute Beiträge, die ich hervorheben werde:
Umkehrung der Kontrolle
Obwohl es nicht gerade mit der Callback-Hölle zu tun hat, kann es Ihnen bei der Gesamtarchitektur helfen, insbesondere bei den Unit-Tests.
Die beiden Hauptunterversionen von Inversion-of-Control sind Dependency Injection und Service Locator. Ich finde, dass der Service Locator in JavaScript im Gegensatz zur Abhängigkeitsinjektion am einfachsten ist. Warum? Hauptsächlich, weil JavaScript eine dynamische Sprache ist und keine statische Typisierung existiert. Java und C # sind unter anderem für die Abhängigkeitsinjektion "bekannt", da sie Typen erkennen können und Schnittstellen, Klassen usw. eingebaut haben. Dies macht die Sache ziemlich einfach. Sie können diese Funktionalität jedoch in JavaScript neu erstellen. Sie wird jedoch nicht identisch sein und ist ein bisschen kitschig. Ich bevorzuge die Verwendung eines Service-Locators in meinen Systemen.
Jede Art von Inversion-of-Control entkoppelt Ihren Code dramatisch in separate Module, die jederzeit verspottet oder gefälscht werden können. Entwickelt eine zweite Version Ihrer Rendering-Engine? Genial, ersetze einfach das alte Interface durch das neue. Service-Locators sind besonders interessant für die neuen Harmony-Proxies, die jedoch nur in Node.js effektiv verwendet werden können. Sie bieten eine schönere API, anstatt Service.get('render');
und zu verwenden Service.render
. Ich arbeite derzeit an dieser Art von System: https://github.com/TheHydroImpulse/Ettore .
Obwohl das Fehlen der statischen Typisierung (statische Typisierung ist ein möglicher Grund für die effektive Verwendung von Dependency Injection in Java, C # und PHP - Es ist nicht statisch typisiert, hat aber Tippfehler) möglicherweise als negativer Punkt angesehen wird, können Sie dies tun Machen Sie es auf jeden Fall zu einem starken Punkt. Da alles dynamisch ist, können Sie ein "falsches" statisches System entwickeln. In Kombination mit einem Service-Locator kann jede Komponente / Modul / Klasse / Instanz an einen Typ gebunden sein.
var Service, componentA;
function Manager() {
this.instances = {};
}
Manager.prototype.get = function(name) {
return this.instances[name];
};
Manager.prototype.set = function(name, value) {
this.instances[name] = value;
};
Service = new Manager();
componentA = {
type: "ship",
value: new Ship()
};
Service.set('componentA', componentA);
// DI
function World(ship) {
if (ship === Service.matchType('ship', ship))
this.ship = new ship();
else
throw Error("Wrong type passed.");
}
// Use Case:
var worldInstance = new World(Service.get('componentA'));
Ein vereinfachtes Beispiel. Für eine effektive Nutzung in der realen Welt müssen Sie dieses Konzept weiterentwickeln. Es kann jedoch hilfreich sein, Ihr System zu entkoppeln, wenn Sie wirklich eine herkömmliche Abhängigkeitsinjektion wünschen. Möglicherweise müssen Sie sich ein wenig mit diesem Konzept beschäftigen. Ich habe nicht viel über das vorherige Beispiel nachgedacht.
Model View Controller
Das offensichtlichste und am häufigsten im Web verwendete Muster. Vor ein paar Jahren war JQuery der letzte Schrei und so wurden JQuery-Plugins geboren. Auf der Client-Seite war kein vollständiges Framework erforderlich. Verwenden Sie einfach jquery und einige Plugins.
Jetzt gibt es einen großen Krieg um das clientseitige JavaScript-Framework. Die meisten von ihnen verwenden das MVC-Muster und alle verwenden es unterschiedlich. MVC ist nicht immer gleich implementiert.
Wenn Sie die traditionellen prototypischen Schnittstellen verwenden, fällt es Ihnen möglicherweise schwer, einen syntaktischen Zucker oder eine nette API zu erhalten, wenn Sie mit MVC arbeiten, es sei denn, Sie möchten manuell arbeiten. Ember.js löst dieses Problem, indem ein "Klassen" / Objekt "-System erstellt wird. Ein Controller könnte folgendermaßen aussehen:
var Controller = Ember.Controller.extend({
index: function() {
// Do something....
}
});
Die meisten clientseitigen Bibliotheken erweitern das MVC-Muster auch durch die Einführung von Ansichtshilfen (werden zu Ansichten) und Vorlagen (werden zu Ansichten).
Neue JavaScript-Funktionen:
Dies ist nur dann effektiv, wenn Sie Node.js verwenden, es ist jedoch von unschätzbarem Wert. Dieser Vortrag auf der NodeConf von Brendan Eich bringt einige coole neue Funktionen. Die vorgeschlagene Funktionssyntax und insbesondere die Bibliothek Task.js js.
Dies wird wahrscheinlich die meisten Probleme mit der Verschachtelung von Funktionen beheben und aufgrund des fehlenden Funktionsaufwands eine etwas bessere Leistung bringen.
Ich bin mir nicht sicher, ob V8 dies nativ unterstützt. Zuletzt habe ich überprüft, ob Sie einige Flags aktivieren müssen, aber dies funktioniert in einem Port von Node.js, der SpiderMonkey verwendet .
Zusätzliche Ressourcen: