Po wielu edycjach ta odpowiedź stała się długa. Z góry przepraszam.
Przede wszystkim eval()
nie zawsze jest zły i może przynieść korzyści w zakresie wydajności, na przykład w przypadku leniwej oceny. Leniwa ocena jest podobna do leniwego ładowania, ale zasadniczo przechowujesz swój kod w ciągach znaków, a następnie używasz eval
lub oceniasz new Function
kod. Jeśli użyjesz sztuczek, stanie się znacznie bardziej użyteczny niż zło, ale jeśli tego nie zrobisz, może to prowadzić do złych rzeczy. Możesz spojrzeć na mój system modułów, który używa tego wzorca: https://github.com/TheHydroImpulse/resolve.js . Resolve.js używa eval zamiast new Function
przede wszystkim do modelowania CommonJS exports
i module
zmiennych dostępnych w każdym module, i new Function
zawija twój kod w funkcję anonimową, jednak ostatecznie pakuję każdy moduł w funkcję, robię to ręcznie w połączeniu z eval.
Więcej na ten temat przeczytasz w dwóch poniższych artykułach, później również w pierwszym.
Generatory harmonii
Teraz, gdy generatory wreszcie wylądowały w V8, a więc w Node.js, pod flagą ( --harmony
lub --harmony-generators
). To znacznie zmniejsza ilość piekła zwrotnego, które masz. To sprawia, że pisanie kodu asynchronicznego jest naprawdę świetne.
Najlepszym sposobem na wykorzystanie generatorów jest zastosowanie biblioteki kontroli przepływu. Umożliwi to przepływ, aby kontynuować pracę w miarę generowania zysków w generatorach.
Podsumowanie / przegląd:
Jeśli nie znasz generatorów, są to praktyki polegające na wstrzymywaniu wykonywania funkcji specjalnych (zwanych generatorami). Ta praktyka nazywana jest plonem za pomocą yield
słowa kluczowego.
Przykład:
function* someGenerator() {
yield []; // Pause the function and pass an empty array.
}
Dlatego za każdym razem, gdy wywołasz tę funkcję po raz pierwszy, zwróci ona nową instancję generatora. Umożliwia to wywołanie next()
tego obiektu w celu uruchomienia lub wznowienia działania generatora.
var gen = someGenerator();
gen.next(); // { value: Array[0], done: false }
Dzwoniłbyś next
do momentu done
powrotu true
. Oznacza to, że generator całkowicie zakończył wykonywanie i nie ma już żadnych yield
instrukcji.
Kontrola przepływu:
Jak widać, kontrolowanie generatorów nie jest automatyczne. Musisz ręcznie kontynuować każdy z nich. Dlatego używane są biblioteki kontroli przepływu, takie jak co .
Przykład:
var co = require('co');
co(function*() {
yield query();
yield query2();
yield query3();
render();
});
Pozwala to na możliwość napisania wszystkiego w Node (i przeglądarce z Regeneratorem Facebooka, który pobiera, jako dane wejściowe, kod źródłowy, który wykorzystuje generatory harmonii i dzieli w pełni kompatybilny kod ES5) w stylu synchronicznym.
Generatory są wciąż całkiem nowe, dlatego wymagają Node.js> = v11.2. Gdy piszę to, wersja 0..11.x jest nadal niestabilna, dlatego wiele natywnych modułów jest uszkodzonych i będzie działać do wersji v.12, gdzie natywny interfejs API się uspokoi.
Aby dodać do mojej oryginalnej odpowiedzi:
Ostatnio wolę bardziej funkcjonalny interfejs API w JavaScript. W razie potrzeby konwencja korzysta z funkcji OOP, ale upraszcza wszystko.
Weźmy na przykład system widoku (klient lub serwer).
view('home.welcome');
Jest o wiele łatwiejszy do odczytania lub śledzenia niż:
var views = {};
views['home.welcome'] = new View('home.welcome');
view
Funkcja po prostu sprawdza, czy ten sam widok już istnieje w lokalnej mapie. Jeśli widok nie istnieje, utworzy nowy widok i doda nowy wpis do mapy.
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 = {};
Niezwykle podstawowy, prawda? Uważam, że radykalnie upraszcza publiczny interfejs i ułatwia korzystanie z niego. Używam również umiejętności łańcuchowych ...
view('home.welcome')
.child('menus')
.child('auth')
Tower, framework, który rozwijam (z kimś innym) lub opracowuję następną wersję (0.5.0), będzie wykorzystywać to funkcjonalne podejście w większości swoich interfejsów.
Niektóre osoby wykorzystują włókna jako sposób na uniknięcie „piekła zwrotnego”. Jest to zupełnie inne podejście do JavaScript i nie jestem jego wielkim fanem, ale korzysta z niego wiele platform / platform; w tym Meteor, ponieważ traktują Node.js jako platformę wątek / na połączenie.
Wolę użyć abstrakcyjnej metody, aby uniknąć piekła zwrotnego. Może to stać się kłopotliwe, ale znacznie upraszcza rzeczywisty kod aplikacji. Pomagając w budowie frameworka TowerJS , rozwiązaliśmy wiele naszych problemów, oczywiście nadal będziesz mieć pewien poziom wywołań zwrotnych, ale zagnieżdżanie nie jest głębokie.
// 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
})
});
Przykład naszego, obecnie rozwijanego, systemu routingu i „kontrolerów”, choć dość różniących się od tradycyjnych „podobnych do szyn”. Ale przykład jest niezwykle mocny i minimalizuje liczbę wywołań zwrotnych i sprawia, że wszystko jest dość widoczne.
Problem z tym podejściem polega na tym, że wszystko jest abstrakcyjne. Nic nie działa tak, jak jest i wymaga za tym „frameworka”. Ale jeśli tego rodzaju funkcje i styl kodowania są zaimplementowane w ramach, to jest to ogromna wygrana.
W przypadku wzorców w JavaScript zależy to szczerze. Dziedziczenie jest naprawdę przydatne tylko podczas korzystania z CoffeeScript, Ember lub dowolnego frameworka / infrastruktury „klasowej”. Gdy znajdujesz się w „czystym” środowisku JavaScript, korzystanie z tradycyjnego prototypowego interfejsu działa jak urok:
function Controller() {
this.resource = get('resource');
}
Controller.prototype.index = function(req, res, next) {
next();
};
Ember.js zaczął, przynajmniej dla mnie, stosować inne podejście do konstruowania obiektów. Zamiast konstruować każdą prototypową metodę niezależnie, użyłbyś interfejsu podobnego do modułu.
Ember.Controller.extend({
index: function() {
this.hello = 123;
},
constructor: function() {
console.log(123);
}
});
Wszystkie są różnymi stylami „kodowania”, ale dodają do bazy kodu.
Wielopostaciowość
Polimorfizm nie jest szeroko stosowany w czystym JavaScript, gdzie praca z dziedziczeniem i kopiowanie modelu podobnego do „klasy” wymaga dużej ilości kodu źródłowego.
Projektowanie oparte na zdarzeniach / komponentach
Modele oparte na zdarzeniach i oparte na komponentach to zwycięskie IMO lub najłatwiejsze do pracy, szczególnie podczas pracy z Node.js, który ma wbudowany komponent EventEmitter, jednak implementacja takich emiterów jest trywialna, to tylko miły dodatek .
event.on("update", function(){
this.component.ship.velocity = 0;
event.emit("change.ship.velocity");
});
To tylko przykład, ale jest to przyjemny model do pracy. Zwłaszcza w projekcie zorientowanym na grę / komponent.
Projektowanie komponentów jest odrębną koncepcją samą w sobie, ale myślę, że działa wyjątkowo dobrze w połączeniu z systemami zdarzeń. Gry są tradycyjnie znane z projektowania opartego na komponentach, gdzie programowanie obiektowe zabiera cię tylko do tej pory.
Projekt oparty na komponentach ma swoje zastosowania. To zależy od rodzaju systemu twojego budynku. Jestem pewien, że działałby z aplikacjami internetowymi, ale działałby wyjątkowo dobrze w środowisku gier, ze względu na liczbę obiektów i oddzielnych systemów, ale z pewnością istnieją inne przykłady.
Wzór Pub / Sub
Wiązanie zdarzeń i pub / sub jest podobny. Wzorzec pub / sub naprawdę świeci w aplikacjach Node.js ze względu na język ujednolicający, ale może działać w dowolnym języku. Działa bardzo dobrze w aplikacjach, grach itp. W czasie rzeczywistym
model.subscribe("message", function(event){
console.log(event.params.message);
});
model.publish("message", {message: "Hello, World"});
Obserwator
Może to być subiektywne, ponieważ niektórzy ludzie myślą o schemacie Observer jako pub / sub, ale mają swoje różnice.
„Obserwator to wzorzec projektowy, w którym obiekt (znany jako przedmiot) utrzymuje listę obiektów w zależności od niego (obserwatorów), automatycznie powiadamiając ich o wszelkich zmianach stanu”. - Wzór obserwatora
Wzorzec obserwatora jest o krok dalej niż typowe systemy pub / sub. Obiekty mają ze sobą ścisłe relacje lub metody komunikacji. Obiekt „Temat” prowadziłby listę osób zależnych „Obserwatorów”. Osoba badana będzie na bieżąco aktualizować swoich obserwatorów.
Programowanie reaktywne
Programowanie reaktywne jest mniejszą, bardziej nieznaną koncepcją, szczególnie w JavaScript. Istnieje jeden framework / biblioteka (o których wiem), który udostępnia łatwą do pracy z API funkcję „reaktywnego programowania”.
Zasoby dotyczące programowania reaktywnego:
Zasadniczo ma zestaw danych synchronizujących (zmiennych, funkcji itp.).
var a = 1;
var b = 2;
var c = a + b;
a = 2;
console.log(c); // should output 4
Uważam, że programowanie reaktywne jest znacznie ukryte, szczególnie w imperatywnych językach. To niezwykle potężny paradygmat programowania, szczególnie w Node.js. Meteor stworzył własny silnik reaktywny, na którym w zasadzie oparty jest szkielet. Jak reaguje Meteor za kulisami? to świetny przegląd tego, jak działa wewnętrznie.
Meteor.autosubscribe(function() {
console.log("Hello " + Session.get("name"));
});
Wykona się to normalnie, wyświetlając wartość name
, ale jeśli ją zmienimy
Session.set („name”, „Bob”);
Ponownie wyświetli wyświetlanie pliku console.log Hello Bob
. Podstawowy przykład, ale możesz zastosować tę technikę do modeli danych i transakcji w czasie rzeczywistym. Za pomocą tego protokołu możesz tworzyć niezwykle wydajne systemy.
Meteor's ...
Wzorzec reaktywny i wzorzec obserwatora są dość podobne. Główna różnica polega na tym, że wzorzec obserwatora często opisuje przepływ danych z całymi obiektami / klasami w porównaniu z programowaniem reaktywnym zamiast tego opisuje przepływ danych do określonych właściwości.
Meteor jest doskonałym przykładem programowania reaktywnego. To środowisko wykonawcze jest nieco skomplikowane z powodu braku JavaScript w natywnych zdarzeniach zmiany wartości (zmieniają to serwery proxy Harmony). Inne frameworki po stronie klienta, Ember.js i AngularJS również wykorzystują programowanie reaktywne (do pewnego stopnia).
Dwie późniejsze struktury wykorzystują wzorzec reaktywny przede wszystkim w swoich szablonach (czyli automatycznych aktualizacjach). Angular.js używa prostej techniki „brudnego sprawdzania”. Nie nazwałbym tego dokładnie reaktywnym programowaniem, ale jest blisko, ponieważ brudne sprawdzanie nie odbywa się w czasie rzeczywistym. Ember.js stosuje inne podejście. Ember użycie set()
i get()
metody, które pozwalają im natychmiast aktualizować wartości. Z ich runloop jest niezwykle wydajny i pozwala na bardziej zależne wartości, gdzie kąt ma teoretyczną granicę.
Obietnice
Nie jest to poprawka do wywołań zwrotnych, ale usuwa wcięcia i ogranicza zagnieżdżone funkcje do minimum. Dodaje także ładną składnię do problemu.
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
});
Możesz także rozłożyć funkcje wywołania zwrotnego, aby nie były wbudowane, ale to kolejna decyzja projektowa.
Innym podejściem byłoby połączenie zdarzeń i obietnic, w których miałbyś funkcję, aby odpowiednio wywołać zdarzenia, wtedy rzeczywiste funkcje funkcjonalne (te, które mają w sobie prawdziwą logikę) wiązałyby się z określonym zdarzeniem. Następnie przekazałbyś metodę dyspozytora wewnątrz każdej pozycji wywołania zwrotnego, musiałbyś jednak opracować pewne zagięcia, które przychodzą na myśl, takie jak parametry, wiedząc, do której funkcji wysłać itp.
Funkcja jednofunkcyjna
Zamiast mieć ogromny bałagan wywołany piekłem zwrotnym, zachowaj jedną funkcję dla jednego zadania i wykonaj to zadanie dobrze. Czasami możesz wyprzedzić siebie i dodać więcej funkcji w ramach każdej funkcji, ale zadaj sobie pytanie: czy może to stać się niezależną funkcją? Nazwij funkcję, a to wyczyści wcięcie, w wyniku czego rozwiąże problem z piekłem zwrotnym.
Na koniec sugeruję opracowanie lub użycie małego „frameworka”, po prostu szkieletu aplikacji i poświęcenie czasu na tworzenie abstrakcji, wybór systemu opartego na zdarzeniu lub „mnóstwo małych modułów, które są niezależny system. Pracowałem z kilkoma projektami Node.js, w których kod był wyjątkowo niechlujny, w szczególności z piekłem zwrotnym, ale także brakiem przemyśleń, zanim zaczęły kodować. Poświęć trochę czasu na przemyślenie różnych możliwości w zakresie API i składni.
Ben Nadel napisał kilka naprawdę dobrych postów na blogu o JavaScript oraz kilka dość surowych i zaawansowanych wzorców, które mogą działać w twojej sytuacji. Kilka dobrych postów, które podkreślę:
Odwrócenie sterowania
Chociaż nie jest to dokładnie związane z piekłem zwrotnym, może pomóc w ogólnej architekturze, szczególnie w testach jednostkowych.
Dwie główne pod-wersje odwrócenia kontroli to Dependency Injection i Service Locator. Uważam, że Service Locator jest najłatwiejszy w JavaScript, w przeciwieństwie do wstrzykiwania zależności. Dlaczego? Głównie dlatego, że JavaScript jest językiem dynamicznym i nie istnieje żadne statyczne pisanie. Java i C # są, między innymi, „znane” z wstrzykiwania zależności, ponieważ są w stanie wykryć typy i mają wbudowane interfejsy, klasy itp. To sprawia, że jest to dość łatwe. Możesz jednak odtworzyć tę funkcję w JavaScript, jednak nie będzie ona identyczna i nieco zhackowana, wolę używać lokalizatora usług w moich systemach.
Każdy rodzaj inwersji kontroli radykalnie rozdzieli kod na osobne moduły, które można w każdej chwili wyśmiewać lub sfałszować. Zaprojektowałeś drugą wersję swojego silnika renderującego? Wspaniale, po prostu zastąp stary interfejs nowym. Lokalizatory usług są szczególnie interesujące z nowymi serwerami Harmony Proxies, jednak tylko efektywnie użytecznymi w Node.js, zapewniają ładniejsze API, zamiast używać Service.get('render');
i zamiast tego Service.render
. Obecnie pracuję nad tego rodzaju systemem: https://github.com/TheHydroImpulse/Ettore .
Chociaż brak pisania statycznego (pisanie statyczne jest możliwym powodem efektywnych zastosowań we wstrzykiwaniu zależności w Javie, C #, PHP - nie jest to pisanie statyczne, ale ma podpowiedzi). Można postrzegać jako punkt ujemny, zdecydowanie zamień to w mocną stronę. Ponieważ wszystko jest dynamiczne, możesz zaprojektować „fałszywy” układ statyczny. W połączeniu z lokalizatorem usług można powiązać każdy komponent / moduł / klasę / instancję z typem.
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'));
Prosty przykład. W rzeczywistym świecie, efektywne wykorzystanie, musisz rozwinąć tę koncepcję, ale może pomóc oddzielić system, jeśli naprawdę chcesz tradycyjnego wstrzykiwania zależności. Być może będziesz musiał trochę pogrzebać przy tej koncepcji. Nie zastanawiałem się nad poprzednim przykładem.
Model-View-Controller
Najbardziej oczywisty wzór i najczęściej używany w sieci. Kilka lat temu JQuery był cały czas na topie, więc narodziły się wtyczki JQuery. Po stronie klienta nie potrzebowałeś pełnego frameworka, po prostu użyj jquery i kilku wtyczek.
Teraz jest ogromna wojna ramowa JavaScript po stronie klienta. Większość z nich używa wzorca MVC i wszyscy używają go inaczej. MVC nie zawsze jest implementowane tak samo.
Jeśli korzystasz z tradycyjnych interfejsów prototypowych, możesz mieć trudności z uzyskaniem cukru syntaktycznego lub dobrego API podczas pracy z MVC, chyba że chcesz wykonać pracę ręczną. Ember.js rozwiązuje ten problem, tworząc system „klasy” / obiektu. Kontroler może wyglądać następująco:
var Controller = Ember.Controller.extend({
index: function() {
// Do something....
}
});
Większość bibliotek po stronie klienta rozszerza również wzorzec MVC, wprowadzając pomocniki widoku (stają się widokami) i szablony (stają się widokami).
Nowe funkcje JavaScript:
Będzie to skuteczne tylko, jeśli korzystasz z Node.js, ale mimo to jest nieocenione. Ta rozmowa Brendana Eicha na NodeConf przynosi nowe, fajne funkcje. Proponowana składnia funkcji, a zwłaszcza biblioteka js Task.js.
Prawdopodobnie to rozwiąże większość problemów z zagnieżdżaniem funkcji i przyniesie nieco lepszą wydajność z powodu braku narzutu funkcji.
Nie jestem pewien, czy V8 obsługuje to natywnie, ostatnio sprawdziłem, czy potrzebujesz włączyć flagi, ale działa to w porcie Node.js, który używa SpiderMonkey .
Dodatkowe zasoby: