O frameworkach i JavaScript słów kilka

February 26, 2008 by admin

Większe aplikacje JavaScript przez samą swoją złożoność wymagają, aby oprzeć ich kod na frameworku - dla wygody, kompleksowości rozwiązań, supportu, ale przede wszystkim szybkości. Czy wszystkie frameworki się do tego nadają?
Nie zadaje tego pytania przypadkowo - ostatnie miesiące to dla mnie rozwijanie Drawtera - dużej i złożonej webaplikacji, która możliwościami i wygodą przerasta ;-) niejedną aplikację offline. Postawiłem tutaj na framework jQuery, początkowo oczarowany jego prostotą i wygodą tworzenia kodu. Powstał na tym blogu nawet czteroodcinkowy (na razie) kurs tego narzędzia. Z dzisiejszej perspektywy uważam, że bezgraniczne ufanie jQuery jest zwyczajnym głupstwem.
Możliwości to nie wszystko
Nie ganie tutaj w żadnym razie małych możliwości tego frameworka - są one wystarczające, by zrobić z nimi, co się nam żywnie podoba. Gdyby było inaczej, wyrzuciłbym go z pamięci po kilku minutach. To jasne. W zasadzie jQuery oferuje w większości to, co jest dzisiaj standardem, ugruntowanym przez Prototype. Trzeba przyznać, że to ciekawe zjawisko - framework, który w większości wytyczył ścieżki, jeśli chodzi o frameworki JS, zostaje wyparty w słupkach popularności przez oferujący to samo, a może nawet mniej, jQuery. Nie trzeba znać obu frameworków bardzo dobrze, by stwierdzić, że główną tego przyczyną była prostota jQuery.
Podsumowując wątek, kierując się możliwościami, stawiam jQuery na podium. To bardzo dobry framework, szczególnie dla małych rozwiązań. Sedno sprawy rozbija się jednak nieco o inne kryteria. O szybkość działania.
Need for speed
W ostatnim półroczu modna była dyskusja na temat szybkości najpopularniejszych frameworków JS. Powstał nawet test prędkości (opierający się na pobraniu elementów DOM, kierując się selektorami, XPath itd.), w którym jQuery musiało w wielu przypadkach uznać wyższość konkurentów: MooTools i Prototype. Potem światło dzienne ujrzała poprawiona wersja jQuery (1.21) i znów zaczął się pusty lament - o wiele szybsze, czy tylko szybsze?
Szczerze mówiąc, tego typu testy są fajne, bo dają jakiś tam - mniej lub bardziej zachwiany, obraz danego frameworka. Chyba nie muszę udowadniać, jak sukces w speed teście napędza popularność narzędzia. I nawet nie chce myśleć, co stałoby się, gdyby jutro wyszedł na świat framework XXX i pobił konkurencję w każdej kategorii o 1 milisekundę…
Testy szybkości do lamusa
Z drugiej strony, SlickSpeed jest zbyt banalny, aby brać go na poważnie. Jestem daleki od uznania, że programiści na świecie dostali olśnienia i zaczęli masowo przenosić przyzwyczajenia z kodowania CSS na swoje skrypty JS. W związku z tym chętnie poznam kogoś, kto swoje projekty opiera na używaniu podobnych selektorów: div div div czy #id .klasa #id.klasa. Webdeveloperzy to w większości (tym bardziej, jeśli są świadomi korzyści płynących z używania frameworka) logicznie myślący ludzie, którzy szukają jak najbardziej optymalnych rozwiązań. Śmiem zaryzykować więc, że najczęściej używanymi selektorami, które sprawiają dość “dużo” kłopotów frameworkom będą np. takie: div .dialog czy div > div. A teraz popatrzcie sobie na szybkość frameworków w tych kategoriach. Jest duża!
Było nie było, nie chodzi nawet o złożoność selektorów. Nie zawsze kod JS produkują rasowi programiści, a w zamian robią to ludzie od CSS i XHTML, używający tego, co im wygodne. A niech sobie robią, bo naprawdę trzeba “umieć” zahamować skrypt samymi selektorami… W większości bowiem, to nie dostęp do elementów drzewa DOM odgrywa najważniejszą rolę w szybkości działania naszych skryptów.
Szybkość, a najczęstsze zadania
Jak wiemy, nie pobieramy elementów drzewa DOM dla sztuki. Często zmieniamy ich treść, rodziców, style CSS, przypisujemy im zdarzenia. I to jest tak naprawdę kwestia pierwszorzędna. Co z tego, że framework pobierze 300 divów w mgnieniu oka, skoro potem weźmie przykład ze zwycięzcy wyścigu żółwi i doda zdarzenia onmouseover w czasie, kiedy strona powinna być dawno gotowa do użycia. Jest to szczególnie ważne, kiedy tworzymy interfejsy użytkownika, webaplikacje oparte o zdarzenia, czy też efekty wizualne na naszych stronach. Zazwyczaj staramy się optymalizować takie skrypty (działając w ogromnej większości na identyfikatorach, a nie rozbudowanych selektorach).
Wracając do autopsji, początkowo wszelkie operacje na stylach i selektorach #id oparłem na jQuery. Do czasu, kiedy przyszło mi przetestować prędkość działania narzędzia. Niestety, wyniki, jakie ujrzałem spowodowały, że miałem niemały orzech do zgryzienia. Rysowanie choćby jednego diva zajmowało od 70 do 100ms, że nie wspomnę o innych operacjach. W czasie mojej konsternacji na szczęście trafiłem na początkową wersję Drawtera, wykorzystującą tylko czysty JavaScript. Jakież było moje zdziwienie, kiedy zobaczyłem naprawdę szybko działającą aplikację! Kolejne moje kroki były automatyczne - tam, gdzie było to wskazane, zmieniałem kod na czysty JS. Najczęściej używane konstrukcje miały podobną postać:
var element = $(”#node1″);
element.css(”left”, e.pageX);
element.css(”width”, width);
element2 = $(”<div></div>”);
element.append(element2);
Bez chwili zastanowienia zamieniłem wszystkie:
var element = document.getElementById(”node1″);
element.style[’left’] = e.pageX+”px”;
element.style[’width’] = width+”px”;
element2 = document.createElement(”div”);
element.appendChild(element2);
Wyobraźcie sobie teraz, że większość konstrukcji była uruchamiana podczas zdarzenia onmousemove, czyli podczas każdego ruchu kursorem myszki. W efekcie, zmiany dały skok wydajnościowy na poziomie 50%.
Wkrótce potem zacząłem zastanawiać się, dlaczego operacje na stylach tak bardzo obciążają skrypt. Oczywiście kluczem do uzyskania odpowiedzi było zajrzenie w kod źródłowy jQuery. Funkcja, która odpowiada tutaj za ustawienie styli nazywa się attr i ma taką postać:
attr: function(elem, name, value){
var fix = jQuery.isXMLDoc(elem) ? {} : jQuery.props;

// Safari mis-reports the default selected property of a hidden option
// Accessing the parent’s selectedIndex property fixes it
if ( name == “selected” && jQuery.browser.safari )
elem.parentNode.selectedIndex;

// Certain attributes only work when accessed via the old DOM 0 way
if ( fix[name] ) {
if ( value != undefined ) elem[fix[name]] = value;
return elem[fix[name]];
} else if ( jQuery.browser.msie && name == “style” )
return jQuery.attr( elem.style, “cssText”, value );

else if ( value == undefined && jQuery.browser.msie && jQuery.nodeName(elem, “form”) && (name == “action” || name == “method”) )
return elem.getAttributeNode(name).nodeValue;

// IE elem.getAttribute passes even for style
else if ( elem.tagName ) {

if ( value != undefined ) {
if ( name == “type” && jQuery.nodeName(elem,”input”) && elem.parentNode )
throw “type property can’t be changed”;
elem.setAttribute( name, value );
}

if ( jQuery.browser.msie && /href|src/.test(name) && !jQuery.isXMLDoc(elem) )
return elem.getAttribute( name, 2 );

return elem.getAttribute( name );

// elem is actually elem.style … set the style
} else {

// IE actually uses filters for opacity
if ( name == “opacity” && jQuery.browser.msie ) {
if ( value != undefined ) {
// IE has trouble with opacity if it does not have layout
// Force it by setting the zoom level
elem.zoom = 1;

// Set the alpha filter to set the opacity
elem.filter = (elem.filter || “”).replace(/alpha([^)]*)/,”") +
(parseFloat(value).toString() == “NaN” ? “” : “alpha(opacity=” + value * 100 + “)”);
}

return elem.filter ?
(parseFloat( elem.filter.match(/opacity=([^)]*)/)[1] ) / 100).toString() : “”;
}
name = name.replace(/-([a-z])/ig,function(z,b){return b.toUpperCase();});
if ( value != undefined ) elem[name] = value;
return elem[name];
}
},
Nie trzeba wiele, by stwierdzić, że jQuery wykonuje znacznie więcej operacji, niż ustawienie odpowiedniego stylu. Z ciekawości przyjrzałem się MooTools, który wg zwolenników chwali się bardzo dobrą implementacją najprostszych instrukcji (jak np. ustawianie stylów).
setStyle: function(property, value){
switch(property){
case ‘opacity’: return this.setOpacity(parseFloat(value));
case ‘float’: property = (window.ie) ? ’styleFloat’ : ‘cssFloat’;
}
property = property.camelCase();
switch($type(value)){
case ‘number’: if (![’zIndex’, ‘zoom’].contains(property)) value += ‘px’; break;
case ‘array’: value = ‘rgb(’ + value.join(’,') + ‘)’;
}
this.style[property] = value;
return this;
},
Powiedzmy, że tutaj sprawa przedstawia się lepiej, kod jest znacznie przejrzystszy, a instrukcja switch jest o wiele lepszym rozwiązaniem od if. Co ważne, funkcja ta wykonuje to, co ma wykonywać, nie ma tutaj zbędnych rzeczy do sprawdzenia!
Pojedyncza funkcja jednak wiosny nie czyni, przeanalizujmy więc, jak w powyższych frameworkach wyglądałaby implementacja JSowego:
document.getElementById(’node’).style[’margin’] = ‘2px’;
jQuery:
$(”#node”).css(”margin”, “2px”);
Moo Tools:
$(”node”).setStyle(”margin”, “2px”);
Sprawa wygląda bliźniaczo podobnie. Przyjrzyjmy się teraz krok po kroku, co musi zrobić jQuery by ustawić margin równy 2 piksele. Najpierw uruchamiana jest funkcja $(), która pełni rolę nakładki na funkcję init(), a ta wygląda tak:
init: function(selector, context) {
// Make sure that a selection was provided
selector = selector || document;

// Handle HTML strings
if ( typeof selector == “string” ) {
var m = quickExpr.exec(selector);
if ( m && (m[1] || !context) ) {
// HANDLE: $(html) -> $(array)
if ( m[1] )
selector = jQuery.clean( [ m[1] ], context );

// HANDLE: $(”#id”)
else {
var tmp = document.getElementById( m[3] );
if ( tmp )
// Handle the case where IE and Opera return items
// by name instead of ID
if ( tmp.id != m[3] )
return jQuery().find( selector );
else {
this[0] = tmp;
this.length = 1;
return this;
}
else
selector = [];
}

// HANDLE: $(expr)
} else
return new jQuery( context ).find( selector );

// HANDLE: $(function)
// Shortcut for document ready
} else if ( jQuery.isFunction(selector) )
return new jQuery(document)[ jQuery.fn.ready ? “ready” : “load” ]( selector );

return this.setArray(
// HANDLE: $(array)
selector.constructor == Array && selector ||

// HANDLE: $(arraylike)
// Watch for when an array-like object is passed as the selector
(selector.jquery || selector.length && selector != window && !selector.nodeType && selector[0] != undefined && selector[0].nodeType) && jQuery.makeArray( selector ) ||

// HANDLE: $(*)
[ selector ] );
},
Skrypt rozpoznaje, jaki argument podaliśmy (var m = quickExpr.exec(selector);) i podstawia go do funkcji document.getElementById. Następnie uruchamiamy funkcję css:
css: function( key, value ) {
return this.attr( key, value, “curCSS” );
},
Ta radośnie przekierowuje nas do funkcji attr, gdzie czeka nas kolejne kilka warunków if i finałowe ustawienie stylów.
MooTools również uruchamia funkcję $(), która wygląda tak:
function $(el){
if (!el) return null;
if (el.htmlElement) return Garbage.collect(el);
if ([window, document].contains(el)) return el;
var type = $type(el);
if (type == ’string’){
el = document.getElementById(el);
type = (el) ? ‘element’ : false;
}
if (type != ‘element’) return null;
if (el.htmlElement) return Garbage.collect(el);
if ([’object’, ‘embed’].contains(el.tagName.toLowerCase())) return el;
$extend(el, Element.prototype);
el.htmlElement = function(){};
return Garbage.collect(el);
};
Następny znany nam setStyle i po obiedzie.
Podane przykłady nasuwają kilka wniosków i przemyśleń.
Stawiaj na czysty JS, gdy wiesz, czego oczekujesz
Frameworki zapewniają nam szybkość tworzenia kodu, a więc mniej pisania i więcej czasu na nasze pomysły, kosztem większej ilości dołączonego kodu frameworka. Czy jednak zawsze należy korzystać z funkcji, oferowanych przez framework? Moja odpowiedź brzmi przecząco, o ile mamy do czynienia z rozbudowanymi skryptami (które robią nieco więcej, niż pokazywanie i ukrywanie boksów). Jak zobaczyliśmy, uruchamianie funkcji css w jQuery, podczas każdego ruchu kursora myszki to dość nierozważne posunięcie, szczególnie, jeśli mamy na myśli szybkość działania. Dawałoby to przecież kilkaset wywołań trzech, dość obfitych w kod funkcji. Czy tego chce ambitny developer? W przypadku MooTools jest trochę lepiej, choć nic nie zastąpi funkcji, którą moglibyśmy sami sobie napisać, a która byłaby wydajna w 100%, bo robiłaby to, co chcemy:
function $(id) {
return document.getElementById(id); }
Używając powyższej, otrzymalibyśmy analogicznie do przykładu z poprzednich akapitów:
$(”node”).style[’margin’] = “2px”;
Jeśli nie podoba nam się nazwa funkcji $() (np. ze względu kolidowania z jQuery), możemy przecież znaleźć inną, bądź użyć metody wbudowanej we framework - noConflict(). Jej użycie powoduje, że wolna zostaje funkcja $(), a zastępuje ją defaultowo jQuery(). Przykładowe wywołanie wyglądałoby wtedy tak:
jQuery.noConflict();
jQuery(document).ready(function() {});
Oczywiście, jeśli uznalibyśmy, że jQuery to za długi zwrot, możliwe jest jego zastąpienie, np.:
jQuery.noConflict();
$j = jQuery;
$j(document).ready(function() {});
Mootools, ani Prototype niestety nie posiadają takiej metody…
Czyste funkcje DOM potrafią znacznie przyspieszyć skrypt!
Drawter korzystał kilka razy, w dość krytycznych momentach, z funkcji children().each(). Wyszukiwałem w ten sposób pewne elementy, a następnie sprawdzałem ich właściwości. Funkcja ta była uruchamiana często i w testach wydajnościowych zajmowała pierwsze lokaty od końca. Rozwiązanie było tylko jedno: getElementsByTagName. Dość powiedzieć, że skrypt z 500ms zjechał do 100ms. Podobnie jest z append() i kilkoma innymi, “zapchanymi kodem” funkcjami frameworka.
Framework nie dla każdego
Nie chcę, mimo wszystko, tworzyć mitu nieprzydatnych frameworków. Z moich doświadczeń wynika, że frameworki nadają się idealnie do rozwiązań wymagających natychmiastowego, nieskomplikowanego działania. Używając frameworka mamy pewność, że kod był testowany wcześniej przez grupę doświadczonych programistów, dodatkowo przepuszczony był przez wielu testerów. Framework to dana jakość i ułatwienie, które nie przekraczają pewnego poziomu. Ciekawe są natomiast refleksje, płynące z używania ich, gdzie się da.
Bum, jaki towarzyszy jQuery jest dość charakterystyczny. Słyszy się tu i ówdzie coraz częściej, że w jQuery napiszemy wszystko i wszyscy. Framework ten jest porównywany do zbawcy świata JS - napiszemy w nim szybko i łatwo, zrobimy wiele więcej, niż zakładaliśmy ;-). Diabeł tkwi jednak w szczegółach. Fakt jest taki, że do skomplikowanych rozwiązań nie powstał jeszcze framework, który pozwoliłby ominąć czysty JS, oferując gamę w pełni wystarczających funkcji. I długo, bądź wcale nie będzie nim Ext. Bo?
Ponieważ nie frameworki, a właśnie JavaScript jest bardzo wolny. Aplikacje oparte na frameworkach, chyba w żadnym innym nie odbiegają tak wydajnością od czystego kodu, jak w JavaScript. W obecnym kształcie trudno jest podążać w stronę choćby JAVY, która jest wręcz wzorowym przykładem korelacji framework - język. Widoczny jest w światku JavaScript trend, by myśleć i pisać frameworkowo, jednak dopóki zdani jesteśmy na powolną ECMAScript 1.7, platforma (framework) i wydajny sposób jej użycia to nadal główny problem w budowaniu aplikacji JavaScript.
Duża też w tym wszystkim rola developerów przeglądarek. Ta jedyna w swoim rodzaju zależność języka od czynników zewnętrznych (programiści przeglądarki) skazuje JavaScript na dość powolny i niejednostajny rozwój. Pozornie specyfikacja jest taka sama dla wszystkich, jednak szybkość działania diametralnie różna. Jak na razie kierunek wyznacza Safari, z niewiarygodnie szybką implementacją DOM i ECMA. Takiej, a nawet większej prędkości brakuje innym, by mówić JS, myśleć Framework.
Warto odpowiedzieć więc na pytania - co robić z JavaScript? Po co nam frameworki, skoro sam język nie potrafi sprostać temu zagadnieniu? Czy budowanie złożonych aplikacji w JS ma sens? Wreszcie, czy JS musi przegrać walkę z mało lubianymi przez Internautów AIR i Flashem? Byłoby to pyrussowe zwycięstwo oponenta, bo cały czas wydaje mi się, że JS - ze względu na historię i taką, a nie inną postać języka, jest skazany na web, a web jest skazany na JS.


No Comments

No comments yet.

Sorry, the comment form is closed at this time.


Buty Nike Adidas
kompresory Rihanna Kopiarki