Dopo molte modifiche, questa risposta è diventata un mostro di lunghezza. Mi scuso in anticipo.
Prima di tutto, eval()
non è sempre male e può portare benefici in termini di prestazioni se usato nella valutazione pigra, per esempio. La valutazione lenta è simile al caricamento lento, ma essenzialmente si memorizza il codice all'interno di stringhe e quindi si utilizza eval
o new Function
per valutare il codice. Se usi alcuni trucchi, diventerà molto più utile del male, ma se non lo fai, può portare a cose cattive. Puoi guardare il mio sistema di moduli che utilizza questo modello: https://github.com/TheHydroImpulse/resolve.js . Resolve.js utilizza eval anziché new Function
principalmente per modellare CommonJS exports
e le module
variabili disponibili in ciascun modulo e new Function
racchiude il codice all'interno di una funzione anonima, tuttavia, finisco per avvolgere ogni modulo in una funzione che lo faccio manualmente in combinazione con eval.
Ne leggi di più nei due articoli seguenti, i successivi si riferiscono anche al primo.
Generatori di armonia
Ora che i generatori sono finalmente atterrati in V8 e quindi in Node.js, sotto una bandiera ( --harmony
o --harmony-generators
). Questi riducono notevolmente la quantità di inferno di callback che hai. Rende davvero eccezionale la scrittura di codice asincrono.
Il modo migliore per utilizzare i generatori è utilizzare una sorta di libreria del flusso di controllo. Ciò consentirà al flusso di continuare ad andare mentre si cede all'interno dei generatori.
Recap / Panoramica:
Se non hai familiarità con i generatori, è una pratica mettere in pausa l'esecuzione di funzioni speciali (chiamate generatori). Questa pratica si chiama cedere usando la yield
parola chiave.
Esempio:
function* someGenerator() {
yield []; // Pause the function and pass an empty array.
}
Pertanto, ogni volta che si chiama questa funzione per la prima volta, verrà restituita una nuova istanza del generatore. Ciò consente di chiamare next()
quell'oggetto per avviare o riprendere il generatore.
var gen = someGenerator();
gen.next(); // { value: Array[0], done: false }
Continueresti a chiamare next
fino al done
ritorno true
. Questo significa che il generatore ha terminato completamente la sua esecuzione e non ci sono più yield
dichiarazioni.
Controllo-Flow:
Come puoi vedere, il controllo dei generatori non è automatico. Devi continuare manualmente ciascuno. Ecco perché vengono utilizzate le librerie del flusso di controllo come co .
Esempio:
var co = require('co');
co(function*() {
yield query();
yield query2();
yield query3();
render();
});
Ciò consente la possibilità di scrivere tutto in Node (e nel browser con Regenerator di Facebook che accetta, come input, il codice sorgente che utilizza generatori di armonia e divide il codice ES5 pienamente compatibile) con uno stile sincrono.
I generatori sono ancora piuttosto nuovi e quindi richiedono Node.js> = v11.2. Mentre scrivo, v0.11.x è ancora instabile e quindi molti moduli nativi sono rotti e lo saranno fino a v0.12, dove l'API nativa si calmerà.
Per aggiungere alla mia risposta originale:
Di recente ho preferito un'API più funzionale in JavaScript. La convenzione utilizza OOP dietro le quinte quando necessario, ma semplifica tutto.
Prendiamo ad esempio un sistema di visualizzazione (client o server).
view('home.welcome');
È molto più facile da leggere o seguire rispetto a:
var views = {};
views['home.welcome'] = new View('home.welcome');
La view
funzione controlla semplicemente se la stessa vista esiste già in una mappa locale. Se la vista non esiste, creerà una nuova vista e aggiungerà una nuova voce alla mappa.
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 = {};
Estremamente semplice, vero? Trovo che semplifichi notevolmente l'interfaccia pubblica e ne renda più facile l'utilizzo. Impiego anche la capacità di catena ...
view('home.welcome')
.child('menus')
.child('auth')
Tower, un framework che sto sviluppando (con qualcun altro) o sviluppando la prossima versione (0.5.0) utilizzerà questo approccio funzionale nella maggior parte delle sue interfacce di esposizione.
Alcune persone sfruttano le fibre come un modo per evitare "l'inferno di richiamata". È un approccio piuttosto diverso a JavaScript, e non ne sono un grande fan, ma molti framework / piattaforme lo usano; incluso Meteor, poiché trattano Node.js come una piattaforma thread / per connessione.
Preferirei usare un metodo astratto per evitare l'inferno di richiamata. Può diventare ingombrante, ma semplifica notevolmente il codice dell'applicazione reale. Aiutando a costruire il framework TowerJS , ha risolto molti dei nostri problemi, ma ovviamente avrai comunque un certo livello di callback, ma il nesting non è profondo.
// 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
})
});
Un esempio del nostro sistema di routing e "controller", attualmente in fase di sviluppo, sebbene abbastanza diverso dai tradizionali "rails-like". Ma l'esempio è estremamente potente e minimizza la quantità di callback e rende le cose abbastanza evidenti.
Il problema con questo approccio è che tutto è astratto. Nulla funziona così com'è e richiede un "framework" dietro di esso. Ma se questo tipo di funzionalità e stile di codifica è implementato in un framework, allora è una grande vittoria.
Per i modelli in JavaScript, dipende onestamente. L'ereditarietà è davvero utile solo quando si utilizza CoffeeScript, Ember o qualsiasi framework / infrastruttura di "classe". Quando ti trovi in un ambiente JavaScript "puro", l'utilizzo della tradizionale interfaccia prototipo funziona come un incantesimo:
function Controller() {
this.resource = get('resource');
}
Controller.prototype.index = function(req, res, next) {
next();
};
Ember.js ha iniziato, almeno per me, usando un approccio diverso alla costruzione di oggetti. Invece di costruire ogni metodo prototipo in modo indipendente, useresti un'interfaccia simile a un modulo.
Ember.Controller.extend({
index: function() {
this.hello = 123;
},
constructor: function() {
console.log(123);
}
});
Tutti questi sono diversi stili di "codifica", ma aggiungono alla tua base di codice.
Polimorfismo
Il polimorfismo non è ampiamente usato nel puro JavaScript, dove lavorare con l'ereditarietà e copiare il modello simile a "classe" richiede molto codice boilerplate.
Progettazione basata su eventi / componenti
I modelli basati su eventi e basati su componenti sono i vincitori IMO, o i più facili da lavorare, specialmente quando si lavora con Node.js, che ha un componente EventEmitter incorporato, sebbene l'implementazione di tali emettitori sia banale, è solo una bella aggiunta .
event.on("update", function(){
this.component.ship.velocity = 0;
event.emit("change.ship.velocity");
});
Solo un esempio, ma è un bel modello con cui lavorare. Soprattutto in un progetto orientato al gioco / ai componenti.
La progettazione dei componenti è un concetto separato di per sé, ma penso che funzioni estremamente bene in combinazione con i sistemi di eventi. I giochi sono tradizionalmente noti per la progettazione basata su componenti, in cui la programmazione orientata agli oggetti ti porta solo finora.
La progettazione basata su componenti ha i suoi usi. Dipende dal tipo di sistema dell'edificio. Sono sicuro che funzionerebbe con le app Web, ma funzionerebbe molto bene in un ambiente di gioco, a causa del numero di oggetti e di sistemi separati, ma sicuramente esistono altri esempi.
Pub / Sottotitoli
Event-binding e pub / sub è simile. Il modello pub / sub brilla davvero nelle applicazioni Node.js a causa del linguaggio unificante, ma può funzionare in qualsiasi lingua. Funziona molto bene in applicazioni in tempo reale, giochi, ecc.
model.subscribe("message", function(event){
console.log(event.params.message);
});
model.publish("message", {message: "Hello, World"});
Osservatore
Questo potrebbe essere soggettivo, poiché alcune persone scelgono di pensare al modello Observer come pub / sub, ma hanno le loro differenze.
"L'Observer è un modello di progettazione in cui un oggetto (noto come soggetto) mantiene un elenco di oggetti a seconda di esso (osservatori), notificandoli automaticamente di eventuali cambiamenti di stato." - Il modello di osservatore
Il modello di osservatore è un passo oltre i tipici pub / sottosistemi. Gli oggetti hanno relazioni strette o metodi di comunicazione tra loro. Un oggetto "Soggetto" manterrebbe un elenco di dipendenti "Osservatori". L'argomento manterrebbe aggiornati gli osservatori.
Programmazione reattiva
La programmazione reattiva è un concetto più piccolo e più sconosciuto, specialmente in JavaScript. Esiste un framework / libreria (di cui sono a conoscenza) che espone un'API di facile utilizzo per utilizzare questa "programmazione reattiva".
Risorse sulla programmazione reattiva:
Fondamentalmente, sta avendo una serie di dati di sincronizzazione (siano essi variabili, funzioni, ecc.).
var a = 1;
var b = 2;
var c = a + b;
a = 2;
console.log(c); // should output 4
Credo che la programmazione reattiva sia notevolmente nascosta, specialmente nei linguaggi imperativi. È un paradigma di programmazione incredibilmente potente, specialmente in Node.js. Meteor ha creato il proprio motore reattivo su cui si basa sostanzialmente il framework. Come funziona la reattività di Meteor dietro le quinte? è un'ottima panoramica di come funziona internamente.
Meteor.autosubscribe(function() {
console.log("Hello " + Session.get("name"));
});
Questo verrà eseguito normalmente, visualizzando il valore di name
, ma se lo cambiamo
Session.set ('name', 'Bob');
Ri-output la visualizzazione console.log Hello Bob
. Un esempio di base, ma è possibile applicare questa tecnica a modelli di dati e transazioni in tempo reale. È possibile creare sistemi estremamente potenti dietro questo protocollo.
Meteor di ...
Il pattern reattivo e il modello Observer sono abbastanza simili. La differenza principale è che il modello di osservatore descrive comunemente il flusso di dati con interi oggetti / classi e la programmazione reattiva descrive invece il flusso di dati verso proprietà specifiche.
Meteor è un ottimo esempio di programmazione reattiva. Il suo runtime è un po 'complicato a causa della mancanza di eventi nativi di modifica del valore in JavaScript (i proxy Harmony lo cambiano). Anche altri framework lato client, Ember.js e AngularJS utilizzano la programmazione reattiva (in parte).
I due quadri successivi utilizzano il modello reattivo, in particolare sui loro modelli (ovvero l'aggiornamento automatico). Angular.js utilizza una semplice tecnica di controllo sporco. Non chiamerei questa programmazione esattamente reattiva, ma è vicina, poiché il controllo sporco non è in tempo reale. Ember.js utilizza un approccio diverso. Uso della brace set()
e get()
metodi che consentono loro di aggiornare immediatamente in base ai valori. Con il loro runloop è estremamente efficiente e consente valori più dipendenti, dove angolare ha un limite teorico.
promesse
Non è una soluzione ai callback, ma elimina alcuni rientri e riduce al minimo le funzioni nidificate. Aggiunge anche una bella sintassi al problema.
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
});
È inoltre possibile diffondere le funzioni di callback in modo che non siano in linea, ma questa è un'altra decisione di progettazione.
Un altro approccio sarebbe quello di combinare eventi e promesse in cui si avrebbe una funzione per inviare gli eventi in modo appropriato, quindi le funzioni funzionali reali (quelle che hanno la vera logica al loro interno) si legerebbero a un evento particolare. Passeresti quindi il metodo dispatcher all'interno di ogni posizione di callback, tuttavia, dovresti elaborare alcuni nodi che verrebbero in mente, come parametri, sapendo a quale funzione spedire, ecc ...
Funzione singola funzione
Invece di avere un enorme casino di callback infernale, mantieni una singola funzione per un singolo compito e svolgi bene quel compito. A volte puoi anticipare te stesso e aggiungere più funzionalità all'interno di ciascuna funzione, ma chiediti: può diventare una funzione indipendente? Assegna un nome alla funzione e questo pulisce il rientro e, di conseguenza, elimina il problema dell'inferno di richiamata.
Alla fine, suggerirei di sviluppare o utilizzare un piccolo "framework", fondamentalmente solo una spina dorsale per la tua applicazione, e prendere tempo per fare astrazioni, decidere su un sistema basato su eventi o un "sacco di piccoli moduli che sono sistema "indipendente". Ho lavorato con diversi progetti Node.js in cui il codice era estremamente disordinato con l'inferno di callback in particolare, ma anche una mancanza di pensiero prima che iniziassero a scrivere codice. Prenditi il tuo tempo per riflettere sulle diverse possibilità in termini di API e sintassi.
Ben Nadel ha pubblicato dei post sul blog davvero validi su JavaScript e alcuni schemi piuttosto severi e avanzati che potrebbero funzionare nella tua situazione. Alcuni buoni post che sottolineerò:
Inversione di controllo
Sebbene non sia esattamente correlato all'inferno del callback, può aiutarti nell'architettura generale, specialmente nei test unitari.
Le due sotto-versioni principali di inversione di controllo sono Iniezione delle dipendenze e Localizzatore di servizi. Trovo Service Locator come il più semplice in JavaScript, al contrario di Iniezione delle dipendenze. Perché? Principalmente perché JavaScript è un linguaggio dinamico e non esiste alcuna digitazione statica. Java e C #, tra gli altri, sono "conosciuti" per l'iniezione di dipendenze perché sei in grado di rilevare i tipi e hanno interfacce, classi, ecc. Integrate. Questo rende le cose abbastanza facili. Puoi, tuttavia, ricreare questa funzionalità all'interno di JavaScript, tuttavia, non sarà identica e un po 'confusa, preferisco usare un localizzatore di servizi all'interno dei miei sistemi.
Qualsiasi tipo di inversione di controllo disaccoppia drasticamente il codice in moduli separati che possono essere derisi o falsificati in qualsiasi momento. Hai progettato una seconda versione del tuo motore di rendering? Fantastico, basta sostituire la vecchia interfaccia con quella nuova. I localizzatori di servizi sono particolarmente interessanti con i nuovi Harmony Proxies, tuttavia, utilizzabili in modo efficace solo all'interno di Node.js, forniscono un'API più bella, anziché utilizzare Service.get('render');
e invece Service.render
. Attualmente sto lavorando su quel tipo di sistema: https://github.com/TheHydroImpulse/Ettore .
Sebbene la mancanza di tipizzazione statica (la tipizzazione statica sia una possibile ragione per gli usi efficaci nell'iniezione di dipendenza in Java, C #, PHP - Non è tipizzata staticamente, ma ha suggerimenti di tipo.) Potrebbe essere considerata un punto negativo, è possibile sicuramente trasformarlo in un punto di forza. Poiché tutto è dinamico, è possibile progettare un sistema statico "falso". In combinazione con un localizzatore di servizi, è possibile associare ciascun componente / modulo / classe / istanza a un tipo.
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'));
Un esempio semplicistico. Per un mondo reale, un utilizzo efficace, è necessario portare avanti questo concetto, ma potrebbe aiutare a disaccoppiare il sistema se si desidera davvero l'iniezione di dipendenza tradizionale. Potrebbe essere necessario giocherellare un po 'con questo concetto. Non ho pensato molto all'esempio precedente.
Model-View-Controller
Il modello più evidente e il più utilizzato sul web. Qualche anno fa, JQuery era di gran moda, e così sono nati i plugin JQuery. Non era necessario un framework completo sul lato client, basta usare jquery e alcuni plugin.
Ora, c'è una grande guerra di framework JavaScript lato client. La maggior parte dei quali utilizza il modello MVC e tutti lo usano in modo diverso. MVC non è sempre implementato allo stesso modo.
Se stai usando le interfacce prototipo tradizionali, potresti avere difficoltà a ottenere uno zucchero sintattico o una bella API quando lavori con MVC, a meno che tu non voglia fare un po 'di lavoro manuale. Ember.js risolve questo problema creando un sistema "class" / object ". Un controller potrebbe apparire come:
var Controller = Ember.Controller.extend({
index: function() {
// Do something....
}
});
La maggior parte delle librerie lato client estende anche il modello MVC introducendo view-helpers (diventando view) e template (diventando view).
Nuove funzionalità JavaScript:
Questo sarà efficace solo se stai usando Node.js, ma è comunque prezioso. Questo discorso a NodeConf di Brendan Eich porta alcune nuove fantastiche funzionalità. La sintassi della funzione proposta, e in particolare la libreria js Task.js.
Questo probabilmente risolverà la maggior parte dei problemi con l'annidamento delle funzioni e porterà prestazioni leggermente migliori a causa della mancanza di sovraccarico della funzione.
Non sono troppo sicuro se V8 lo supporta in modo nativo, per ultimo ho verificato che era necessario abilitare alcuni flag, ma questo funziona in una porta di Node.js che utilizza SpiderMonkey .
Risorse extra: