Implementera Twitter Scrolling utan jQuery

jQuery är ett fantastiskt verktyg, men är det någonsin en tid då du inte ska använda den? I denna handledning ska vi titta på hur man bygger ett intressant rullningsbeteende med jQuery och sedan granska hur vårt projekt potentiellt kan förbättras genom att ta bort så mycket jQuery eller abstraktion som möjligt.


Intro: Planen

Ibland kan din kod vara ännu mer? Magisk? genom att subtrahera en del av den jQuery godheten.

Du kan förvänta dig att det här är din normala gör-något-awesome-med-jQuery handledning. Egentligen är det inte. Medan du kanske slutar bygga en ganska cool men, uppriktigt, kanske lika värdelös effekt, det är inte den viktigaste punkten jag vill att du ska ta bort från denna handledning.

Som du förhoppningsvis ser, vill jag att du ska lära dig att titta på jQuery du skriver som bara vanlig JavaScript, och inse att det inte finns något magiskt om det. Ibland kan din kod vara ännu mer? Magisk? genom att subtrahera en del av den jQuery godheten. Förhoppningsvis, i slutet av detta kommer du att bli lite bättre att utveckla med JavaScript än när du började.

Om det låter för abstrakt, överväg det här en lektion i prestanda och kodrefaktor? och stiger också utanför din komfortzon som utvecklare.


Steg 1: Projektet

Här är vad vi ska bygga. Jag fick denna inspiration från den relativt nya Twitter för Mac-appen. Om du har appen (det är gratis), gå till någon persons kontosida. När du rullar ner ser du att de inte har någon avatar till vänster om varje tweet. Avatar för den första tweeten? följer? du som du rullar ner. Om du träffar en retweet ser du att den retweetade personens avatar placeras på rätt sätt bredvid hans eller hennes tweet. Då, när retweeterens tweets börjar igen, tar deras avatar över.

Det här är den funktionalitet som jag ville bygga. Med jQuery skulle det inte vara för svårt att sätta ihop, tänkte jag. Och så började jag.


Steg 2: HTML & CSS

Självklart, innan vi kan komma till showens stjärna, behöver vi lite markup att arbeta med. Jag kommer inte spendera mycket tid här, för det är inte huvudpunkten i denna handledning:

    Twitter Avatar Scrolling    

Det här är något som kvitteraren hade att säga.

Det här är något som kvitteraren hade att säga.

Det här är något som kvitteraren hade att säga.

Det här är något som kvitteraren hade att säga.

Det här är något som kvitteraren hade att säga.

Det här är något som kvitteraren hade att säga.

Det här är något som kvitteraren hade att säga.

Ja, det är stort.

Vad sägs om vissa CSS? Prova detta:

kropp font: 13px / 1.5 "helvetica neue", helvetica, arial, san-serif;  artikel display: block; bakgrund: #ececec; bredd: 380px; padding: 10px; margin: 10px; overflow: hidden;  artikel img float: left;  artikel p marginal: 0; padding-vänster: 60 bildpunkter; 

Ganska minimal, men det kommer att ge oss vad vi behöver.

Nu vidare till JavaScript!


Steg 3: JavaScript, Round 1

Jag tycker att det är rättvist att säga att detta inte är ditt genomsnittliga JavaScript widget-arbete; det är lite mer komplicerat. Här är bara några av de saker du behöver reda på:

  • Du måste gömma varje bild som är densamma som bilden i föregående tweet.?
  • När sidan rullas måste du bestämma vilken tweet? ligger närmast på toppen av sidan.
  • Om "tweet"? är den första i en serie av? tweets? av samma person måste vi fixa avataren på plats, så det rullar inte med resten av sidan.
  • När toppen? Tweet? är den sista i en körning av tweets av en användare, vi måste stoppa avataren på lämplig plats.
  • Allt detta måste fungera för att rulla både ner och uppåt på sidan.
  • Eftersom allt detta genomförs varje gång en scrollhändelse brinner, det måste vara otroligt snabbt.

När du börjar skriva något, oroa dig för att du bara får det att fungera. optimering kan komma senare. Version One ignorerade flera viktiga jQuery bästa praxis. Det vi börjar med här är version två: den optimerade jQuery-koden.

Jag bestämde mig för att skriva detta som ett jQuery-plugin, så det första steget är att bestämma hur det ska kallas; Jag gick med här:

$ (wrapper_element) .twitter_scroll (entries_element, unscrollable_element);

Det jQuery-objekt vi kallar plugin på, sveper "tweetsna"? (eller vad du rullar igenom). Den första parametern som plugin tar är en väljare för de element som kommer att bläddra: de? Tweets.? Den andra väljaren är för de element som håller på plats när det behövs (plugin förväntar sig att dessa är bilder, men det bör inte ta mycket justering så att det fungerar för andra element). Så, för den HTML som vi hade ovan kallar vi plugin så här:

$ ("avsnitt"). twitter_scroll ("artikel", ".avatar"); // OR $ ("section"). Twitter_scroll (". Avatar");

Som du ser när vi kommer till koden kommer den första parametern att vara frivillig; Om det bara finns en parameter, antar vi att det är den oscrollbara väljaren, och posterna är de direkta föräldrarna till unscrollablesna (jag vet att unscrollables är ett dåligt namn, men det är det mest generiska jag kan komma med).

Så här är vårt plugin-skal:

(funktion ($) jQuery.fn.twitter_scroll = funktion (poster, unscrollable) ; (jQuery));

Hädanefter kommer alla JavaScript vi tittar på att gå in här.

Plugin Set-Up

Låt oss börja med uppställningskoden; Det finns lite arbete att göra innan vi ställer in rullningsbehandlaren.

om (! unscrollable) unscrollable = $ (poster); poster = unscrollable.parent ();  annars unscrollable = $ (unscrollable); poster = $ (poster); 

Först parametrarna: Om unscrollable är ett falskt-y-värde, ställer vi in ​​det till? jQuerified? anteckningar väljare och set anteckningar till föräldrarna till unscrollable. Annars ska vi? JQuerify? båda parametrarna. Det är viktigt att märka att nu (om användaren har gjort sin markering korrekt, vilket vi måste anta att de har), har vi två jQuery-objekt där matchande poster har samma index: så unscrollable [i] är barnet av anteckningar [i]. Detta kommer att vara användbart senare. (Notera: om vi inte ville anta att användaren markerade sitt dokument korrekt eller att de använde väljare som skulle fånga element utanför vad vi vill skulle vi kunna använda detta som kontextparametern, eller använd hitta metod av detta.)

Låt oss nu ange några variabler. Normalt skulle jag göra det här högst upp, men flera beror på unscrollable och anteckningar, så vi behandlade det första:

var parent_from_top = this.offset (). top, entries_from_top = entries.offset (). top, img_offset = unscrollable.offset (), prev_item = null, starter = false, margin = 2, anti_repeater = null, win = $ ), scroll_top = win.scrollTop ();

Låt oss springa igenom dessa. Som du kanske kan tänka dig är kompensationen för de element vi jobbar med här viktiga. jQuery s offset Metoden returnerar ett objekt med topp och vänster offset egenskaper. För moderelementet (detta inuti plugin) och anteckningar, Vi behöver bara den bästa offseten, så får vi det. För unscrollables behöver vi både topp och vänster, så vi kommer inte att få något specifikt här.

Variablerna prev_item, förrätt, och anti_repeater kommer att användas senare, antingen utanför rullhanteraren eller inuti rullhanteraren, där vi behöver värden som kvarstår i samband med handlarens samtal. Till sist, vinna kommer att användas på några ställen, och scroll_top är avståndet från rullningsfältet från fönstret; Vi använder det senare för att bestämma vilken riktning vi rullar in.

Därefter kommer vi att bestämma vilka element som är första och sista i streck av? Tweets.? Det finns förmodligen ett par sätt att göra detta; Vi ska göra detta genom att ange en HTML5-dataattribut till lämpliga element.

entries.each (funktion (i) var img = unscrollable [i]; om ($ .contains (this, img)) if (img.src === prev_item) img.style.visibility = "hidden"; om (startar) poster [i-1] .setAttribute ("data-8088-starter", "true"); starter = false; om (! poster [i + 1]) poster [i] .setAttribute "data-8088-ender", "true"); annat prev_item = img.src; starter = true; om (poster [i-1] && unscrollable [i-1] .style.visibility === " dolda ") poster [i-1] .setAttribute (" data-8088-ender "," true ");); prev_item = null;

Vi använder jQuery s varje metod på anteckningar objekt. Kom ihåg att inuti funktionen passerar vi in, detta avser det aktuella elementet från anteckningar. Vi har också indexparametern, som vi ska använda. Vi börjar med att få motsvarande element i unscrollable objekt och lagra den i img. Då, om vår post innehåller det här elementet (det ska, men vi bara checkar), kontrollerar vi om källkoden för bilden är densamma som prev_item; Om det är, vet vi att den här bilden är densamma som den i föregående post. Därför kommer vi att gömma bilden; Vi kan inte använda visningsegenskapen eftersom det skulle ta bort det från dokumentets flöde; vi vill inte att andra element flyttar på oss.

Då, om förrätt Det är sant, vi ger anteckningen innan den här egenskapen data 8088-starter; kom ihåg att alla HTML5-dataattribut måste börja med? data- ?; och det är en bra idé att lägga till ditt eget prefix så att du inte kommer att strida mot andra utvecklarers kod (mitt prefix är 8088, du måste hitta din egen :)). HTML5-dataattribut måste vara strängar, men i vårt fall är det inte de data vi är oroade över. vi behöver bara markera det här elementet. Sedan sätter vi på förrätt till falskt. Slutligen, om det här är den sista posten, markerar vi det som ett slut.

Om bildkällan inte är densamma som källan till föregående bild, återställs vi prev_item med källan till din nuvarande bild; då ställer vi start till Sann. På det sättet, om vi finner att nästa bild har samma källa som den här, kan vi markera den här som starteren. Slutligen, om det finns en post före den här, och dess tillhörande bild är dold, vet vi att inmatningen är slutet på ett streck (eftersom den här har en annan bild). Därför ska vi ge det en dataattribut som markerar det som ett slut.

När vi är färdiga så ställer vi in prev_item till null; Vi kommer att återanvända det snart.

Nu, om du tittar på posterna i Firebug, bör du se något så här:

Scroll Handler

Nu är vi redo att arbeta på den här rullhanteraren. Det finns två delar i detta problem. Först hittar vi den post som ligger närmast överst på sidan. För det andra gör vi vad som är lämpligt för bilden relaterad till den posten.

$ (dokument) .bind ("scroll", funktion (e) var temp_scroll = win.scrollTop (), ner = ((scroll_top - temp_scroll) < 0) ? true : false, top_item = null, child = null; scroll_top = temp_scroll; // more coming );

Det här är vårt rullhanteringsskal; Vi har ett par variabler som vi skapar för att börja med; just nu notera ner. Detta kommer att vara sant om vi rullar ner, falskt om vi rullar upp. Då återställer vi scroll_top att vara avståndet från toppen som vi rullas till.

Nu, låt oss sätta top_item att vara posten närmsta toppen av sidan:

top_item = entries.filter (funktion (i) var distance = $ (this). offset (). top - scroll_top; returnera (avstånd < (parent_from_top + margin) && distance > (parent_from_top - margin)); );

Det här är inte svårt alls. vi använder bara filtrera metod för att bestämma vilken post vi vill tilldela top_item. Först får vi distans genom att subtrahera den mängd vi har bläddrat från den högsta förskjutningen av posten. Då återkommer vi sant om distans är mellan parent_from_top + margin och parent_from_top - margin; annars, false. Om det här förvirrar dig, tänk på det så här: vi vill återvända sant när ett element står högst upp i fönstret; I detta fall, distans skulle motsvara 0. Vi måste dock redogöra för den övre förskjutningen av behållaren som omsluter våra tweets ,? så vi vill verkligen distans att lika parent_from_top.

Men det är möjligt att vår rullhanterare inte kommer att skjuta när vi exakt är på den pixeln, men istället när vi är nära den. Jag upptäckte detta när jag loggade avståndet och fann att det var värden som var en halv pixel av; Om din rullhanteringsfunktion inte är för effektiv (som den här inte kommer att bli, ännu inte), är det möjligt att det inte kommer att skjuta på varje bläddra händelse. För att se till att du får en i rätt område lägger vi till eller subtraherar en marginal för att ge oss ett litet intervall att arbeta inom.

Nu när vi har toppunktet, låt oss göra något med det.

om (top_item) if (top_item.attr ("data-8088-starter")) om (! anti_repeater) child = top_item.children (unscrollable.selector); anti_repeater = child.clone (). appendTo (document.body); child.css ("synlighet", "dold"); anti_repeater.css ('position': 'fixed', 'top': img_offset.top + 'px', 'left': img_offset.left + "px");  annars om (top_item.attr ("data-8088-ender")) top_item.children (unscrollable.selector) .css ("synlighet", "synlig"); om (anti_repeater) anti_repeater.remove ();  anti_repeater = null;  om (! ned) if (top_item.attr ("data-8088-starter")) top_item.children (unscrollable.selector) .css ("synlighet", "synlig"); om (anti_repeater) anti_repeater.remove ();  anti_repeater = null;  annars om (top_item.attr ("data-8088-ender")) child = top_item.children (unscrollable.selector); anti_repeater = child.clone (). appendTo (document.body); child.css ("synlighet", "dold"); anti_repeater.css ('position': 'fixed', 'top': img_offset.top + 'px', 'left': img_offset.left + "px"); 

Du kanske märker att ovanstående kod är nästan samma sak två gånger; kom ihåg att detta är den ooptimerade versionen. När jag långsamt arbetade igenom problemet började jag med att räkna ut hur man rullar ner arbetet; När jag hade löst det arbetade jag på att rulla upp. Det var inte omedelbart uppenbart, tills all funktionalitet var på plats, att det skulle finnas så mycket likhet. Kom ihåg, vi optimerar snart.

Så, låt oss dissekera detta. Om toppunktet har en data 8088-starter attribut, låt oss då kontrollera om anti_repeater har ställts in; Detta är variabeln som kommer att peka på bildelementet som kommer att fixas när sidan rullar. Om anti_repeater har inte ställts in, då får vi barnet vårt av top_item post som har samma väljare som unscrollable (nej, det här är inte ett smart sätt att göra det, vi förbättrar det senare). Sedan klonar vi det och lägger det till kroppen. Vi gömmer den där och placerar sedan den klonade en exakt var den ska gå.

Om elementet inte har en data 8088-starter attribut, vi ska checka efter en data 8088-ender attribut. Om det finns där hittar vi rätt barn och gör det synligt och tar sedan bort anti_repeater och ställa in den här variabeln till null.

Lyckligtvis, om vi inte går ner (om vi går upp), är det exakt det omvända för våra två attribut. Och om top_item har inga attribut, vi är någonstans mitt i ett streck och behöver inte ändra någonting.

Utvecklingssamtal

Tja, den här koden gör vad vi vill Men om du försöker det märker du att du måste rulla väldigt långsamt för att det ska fungera korrekt. Försök lägga till raderna console.profile ( "scroll") och console.profileEnd () som första och sista linjerna i rullhanteringsfunktionen. För mig tar handlaren mellan 2,5ms - 4ms, med 166 - 170 funktionssamtal som äger rum.

Det är alldeles för länge för en rullhanterare att springa; och som du kan tänka dig kör jag det här på en ganska välutrustad dator. Observera att vissa funktioner kallas 30-31 gånger; vi har 30 poster som vi jobbar med, så det här är förmodligen en del av loopingen över dem alla för att hitta toppen. Det innebär att ju fler inträden vi har, desto långsammare kommer detta att springa; så ineffektivt! Så måste vi se hur vi kan förbättra detta.


Steg 4: JavaScript, Round 2

Om du misstänker att jQuery är den främsta skyldige här, har du rätt. Medan ramar som jQuery är väldigt hjälpsamma och gör arbetet med DOM en bris, kommer de med ett kompromiss: prestanda. Ja, de blir alltid bättre. och ja, det är också webbläsarna. Vår situation kräver dock den snabbaste möjliga koden, och i vårt fall måste vi ge upp lite jQuery för ett direkt DOM-arbete som inte är alltför svårt.

Scroll Handler

Låt oss med den uppenbara delen: vad vi gör med top_item när vi har hittat det. För närvarande, top_item är ett jQuery-objekt; Men allt vi gör med jQuery med top_item är trivialt hårdare utan jQuery, så gör vi det? raw.? När vi granskar fångsten top_item, vi ska se till att det är en rå DOM-element.

Så här är vad vi kan ändra för att göra det snabbare:

  • Vi kan refactor våra if-statements för att undvika den enorma mängden dubbelarbete (det här är mer av en kod renhet punkt, inte en prestationspunkt).
  • Vi kan använda den infödda getAttribute metod istället för jQuery s attr.
  • Vi kan få elementet från unscrollable som motsvarar top_item post, istället för att använda unscrollable.selector.
  • Vi kan använda den infödda clodeNode och appendChild metoder, istället för jQuery-versionerna.
  • Vi kan använda stil egendom istället för jQuery s css metod.
  • Vi kan använda den infödda removeNode istället för jQuery's ta bort.

Genom att tillämpa dessa idéer slutar vi med detta:

om (top_item) if ((ner && top_item.getAttribute ("data-8088-starter")) || (! ner && top_item.getAttribute ("data-8088-ender"))) if (! anti_repeater)  barn = unscrollable [entries.indexOf (top_item)]; anti_repeater = child.cloneNode (false); document.body.appendChild (anti_repeater); child.style.visibility = "hidden"; stil = anti_repeater.style; style.position = 'fixed'; style.top = img_offset.top + 'px'; style.left = img_offset.left + 'px';  om ((ner && top_item.getAttribute ("data-8088-ender")) || (! ner && top_item.getAttribute ("data-8088-starter"))) unscrollable [entries.indexOf (top_item)] .style.visibility = "visible"; om (anti_repeater) anti_repeater.parentNode.removeChild (anti_repeater);  anti_repeater = null; 

Det här är mycket bättre kod: inte bara blir det av det dubbletteret, men det använder också absolut ingen jQuery. (När jag skriver detta tänker jag att det kanske skulle vara lite snabbare att tillämpa en CSS-klass för att göra stylingen, du kan experimentera med det!)

Du kanske tror att det handlar om så bra som vi kan få; Det finns dock vissa seriösa optimeringar som kan äga rum när det gäller att komma igång top_item. För närvarande använder vi jQuery s filtrera metod. Om du tänker på det är det otroligt dåligt. Vi vet att vi bara kommer att få ett objekt tillbaka från denna filtrering; men filtrera funktion vet inte det, så det fortsätter att köra element genom vår funktion efter att vi hittat den vi vill ha. Vi har 30 element i anteckningar, så det är ett ganska stort slöseri med tid. Vad vi vill göra är detta:

för (i = 0; poster [i]; i ++) distance = $ (poster [i]). offset (). top - scroll_top; om (avstånd < (parent_from_top + margin) && distance > (parent_from_top - margin)) top_item = poster [i]; ha sönder; 

(Alternativt kan vi använda en slinga med villkoret !top_item; hur som helst, det spelar ingen roll så mycket.)

På så sätt, när vi hittar top_item, vi kan sluta söka. Men vi kan göra det bättre; för att rulla är en linjär sak kan vi förutse vilket föremål som ligger närmast toppen nästa. Om vi ​​rullar ner kommer det antingen att vara samma sak som förra gången, eller den efter den. Om vi ​​rullar upp blir det samma föremål som förra gången, eller objektet före det. Det här kommer att vara användbart ju längre ner på sidan vi får, för loopen börjar alltid högst upp på sidan.

Så hur kan vi genomföra detta? Tja, vi måste börja med att hålla reda på vad top_item var under vår tidigare körning av scroll-hanteraren. Vi kan göra det genom att lägga till

prev_item = top_item;

till slutet av rullningsfunktionen. Nu, upp där vi tidigare hittade top_item, sätt detta:

om (prev_item) prev_item = $ (prev_item); höjd = höjd || prev_item.outerHeight (); om (ner && prev_item.offset (). topp - scroll_top + höjd < margin)  top_item = entries[ entries.indexOf(prev_item[0]) + 1];  else if ( !down && (prev_item.offset().top - scroll_top - height) > (margin + parent_from_top)) top_item = poster [entries.indexOf (prev_item [0]) - 1];  annat top_item = prev_item [0];  annat för (i = 0; poster [i]; i ++) avstånd = $ (poster [i]). offset (). top - scroll_top; om (avstånd < (parent_from_top + margin) && distance > (parent_from_top - margin)) top_item = poster [i]; ha sönder; 

Detta är definitivt en förbättring; om det fanns en prev_item, då kan vi använda det för att hitta nästa top_item. Matematiken här blir lite knepig, men det här gör vi:

  • Om vi ​​går ner och föregående objekt är helt överst på sidan, får du nästa element (vi kan använda indexet på prev_item + 1 för att hitta rätt element i anteckningar).
  • Om vi ​​går upp och föregående objekt är tillräckligt långt ner på sidan, hämta föregående post.
  • Annars använder du samma post som förra gången.
  • Om det inte finns något top_item, vi använder vår för loop som en backning.

Det finns några andra saker att ta itu med här. För det första, vad är det här höjd variabel? Tja, om alla våra poster har samma höjd, kan vi undvika att beräkna höjden varje gång i rullhanteraren genom att göra det i inställningen. Så jag har lagt till det här i inställningen:

höjd = entries.outerHeight (); om (entries.length! == entries.filter (funktion () return $ (this) .outerHeight () === height;). längd) height = null; 

jQuery s outerHeight Metoden får höjden av ett element, inklusive det är vaddering och gränser. Om alla element har samma höjd som den första, då höjd kommer att ställas in; annat höjd kommer att vara null. Då när du får top_item, vi kan använda höjd om den är inställd.

Det andra att notera är detta: du kanske tror att det är ineffektivt att göra prev_item.offset (). top dubbelt; Bör inte det gå in i en variabel. Faktum är att vi bara gör det en gång, för den andra om uttalandet kommer bara att ringas om ner är falskt; Eftersom vi använder den logiska OCH-operatören, kommer de andra delarna av de två om de aldrig kommer att köras på samma funktionssamtal. Naturligtvis kan du lägga den i en variabel om du tycker att den håller din kod renare, men det gör inget för prestanda.

Men jag är fortfarande inte nöjd. Visst, vår rullhanterare är snabbare nu, men vi kan göra det bättre. Vi använder bara jQuery för två saker: för att få outerHeight av prev_item och för att få det bästa offsetet av prev_item. För något så litet är det ganska dyrt att göra ett jQuery-objekt. Det finns emellertid inte en direkt uppenbar, icke-jQuery-lösning för dessa, som det fanns tidigare. Så, låt oss dyka in i jQuery-källkoden för att se exakt vad jQuery gör. På så sätt kan vi dra ut de bitar vi behöver utan att använda den dyra vikten av jQuery själv.

Låt oss börja med det största offsetproblemet. Här är jQuery-koden för offset metod:

funktion (alternativ) var elem = detta [0]; om (! elem ||! elem.ownerDocument) return null;  om (alternativ) return this.each (funktion (i) jQuery.offset.setOffset (detta alternativ, i););  om (elem === elem.ownerDocument.body) returnera jQuery.offset.bodyOffset (elem);  var box = elem.getBoundingClientRect (), doc = elem.ownerDocument, body = doc.body, docElem = doc.documentElement, clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0, topp = box.top + (self.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop) - clientTop, left = box.left + (self.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft) - clientLeft; returnera topp: topp, vänster: vänster; 

Det ser komplicerat ut, men det är inte så illa. vi behöver bara den övre förskjutningen, inte den vänstra förskjutningen, så vi kan dra ut det här och använda det för att skapa vår egen funktion:

funktion get_top_offset (elem) var doc = elem.ownerDocument, body = doc.body, docElem = doc.documentElement; returnera elem.getBoundingClientRect (). top + (self.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop) - docElem.clientTop || body.clientTop || 0; 

Allt vi behöver göra är att passera i det råa DOM-elementet, och vi kommer att få toppen offset tillbaka; bra! så vi kan ersätta alla våra offset (). top samtal med den här funktionen.

Vad sägs om outerHeight? Den här är lite svårare eftersom den använder en av de flera användningsområdena intern jQuery-funktioner.

funktion (marginal) returnera detta [0]? jQuery.css (denna [0], typ, false, margin? "margin": "border"): null; 

Som vi kan se gör detta faktiskt bara ett samtal till css funktion, med dessa parametrar: elementet,? höjd ?,? gräns ?, och falsk. Så här ser det ut:

funktion (elem, namn, kraft, extra) if (name === "width" || namn === "height") var val, props = cssShow, which = name === "width"? cssWidth: cssHeight; funktion getWH () val = namn === "bredd"? elem.offsetWidth: elem.offsetHeight; om (extra === "gränsen") return;  jQuery.each (vilken funktion () if (! extra) val - = parseFloat (jQuery.curCSS (elem, "padding" + detta, sant)) || 0; om (extra === "margin ") val + = parseFloat (jQuery.curCSS (elem," margin "+ detta, sant)) || 0; annars val - = parseFloat (jQuery.curCSS (elem," border "+ this +" Width " , sant)) || 0;);  om (elem.offsetWidth! == 0) getWH ();  annars jQuery.swap (elem, rekvisita, getWH);  returnera Math.max (0, Math.round (val));  returnera jQuery.curCSS (elem, namn, kraft); 

Vi kanske verkar hopplöst förlorade vid denna tidpunkt, men det är värt att följa logiken dock? eftersom lösningen är fantastisk! Visas, vi kan bara använda ett element offsetHeight fast egendom; Det ger oss exakt vad vi vill ha!

Därför ser vår kod nu ut så här:

om (prev_item) height = height || prev_item.offsetHeight; om (ner && get_top_offset (prev_item) - scroll_top + height < margin)  top_item = entries[ entries.indexOf(prev_item) + 1];  else if ( !down && (get_top_offset(prev_item) - scroll_top - height) > (margin + parent_from_top)) top_item = poster [entries.indexOf (prev_item) - 1];  annat top_item = prev_item;  annat för (i = 0; poster [i]; i ++) distance = get_top_offset (poster [i]) - scroll_top; om (avstånd < (parent_from_top + margin) && distance > (parent_from_top - margin)) top_item = poster [i]; ha sönder; 

Nu behöver ingenting vara ett jQuery-objekt! Och om du kör ner det, borde du vara långt under en millisekund och har bara ett par funktionssamtal.

Användbara verktyg

Innan vi avslutar, här är ett användbart tips för att gräva runt i jQuery som vi gjorde här: använd James Padolsey's jQuery Source Viewer. Det är ett fantastiskt verktyg som gör det otroligt enkelt att gräva runt inuti koden och se vad som händer utan att vara alltför överväldigad. [Sidens anteckning: Ett annat bra verktyg du ska checka ut - jQuery Deconstructer.]


Slutsats: Vad vi har täckt

Jag hoppas att du har haft den här handledningen! Du kanske har lärt dig hur du skapar lite ganska snyggt rullningsbeteende, men det var inte huvudpunkten. Du borde ta bort dessa poäng från denna handledning:

  • Att din kod gör vad du vill betyder inte att det inte kan vara renare, eller snabbare eller på något sätt bättre.
  • jQuery-eller vilket är ditt favoritbibliotek-är bara JavaScript; det kan vara långsamt också; och ibland är det bäst att inte använda det.
  • Det enda sättet du kommer att förbättra som en JavaScript-utvecklare är om du trycker dig ut ur din komfortzon.

Har några frågor? Eller kanske har du en idé om hur man förbättrar detta ännu mer! Låt oss diskutera det i kommentarerna!