Implementeer Twitter Scrolling zonder jQuery

jQuery is een geweldige tool, maar is er ooit een tijd dat je het niet zou moeten gebruiken? In deze zelfstudie gaan we bekijken hoe we een aantal interessante scrolgedragingen met jQuery kunnen opbouwen en bekijken we hoe ons project mogelijk kan worden verbeterd door zoveel mogelijk jQuery of abstractie te verwijderen..


Intro: Het plan

Soms kan je code zelfs nog meer zijn? Magisch? door een deel van die goedheid van jQuery af te trekken.

Je zou verwachten dat dit je normale doe-iets-geweldig-met-jQuery zelfstudie is. Eigenlijk is het dat niet. Hoewel je uiteindelijk een nogal cool, maar eerlijk gezegd, misschien even nutteloos effect kunt opbouwen, is dat niet het belangrijkste punt dat ik wil dat je weghaalt uit deze tutorial.

Zoals je hopelijk zult zien, wil ik dat je leert om te kijken naar de jQuery die je aan het schrijven bent als gewoon JavaScript, en besef dat er niets magisch aan is. Soms kan je code zelfs nog meer zijn? Magisch? door een deel van die goedheid van jQuery af te trekken. Hopelijk ben je tegen het einde een beetje beter in het ontwikkelen met JavaScript dan toen je begon.

Als dat te abstract klinkt, beschouw dit dan als een les in performance en code refactoring? en ook als ontwikkelaar buiten je comfortzone stappen.


Stap 1: Het project

Dit is wat we gaan bouwen. Ik heb deze inspiratie gekregen van de relatief nieuwe Twitter voor Mac-app. Als je de app hebt (het is gratis), ga dan iemands accountpagina bekijken. Terwijl je scrolt, zie je dat ze geen avatar links van elke tweet hebben; de avatar voor de eerste tweet? volgt? jij terwijl je naar beneden scrolt. Als u een retweet tegenkomt, ziet u dat de avatar van de geretweverde persoon op de juiste manier naast zijn of haar tweet is geplaatst. Vervolgens, wanneer de tweets van de retweeter opnieuw beginnen, neemt hun avatar het over.

Dit is de functionaliteit die ik wilde bouwen. Met jQuery zou dit niet al te moeilijk zijn om samen te stellen, dacht ik. En zo begon ik.


Stap 2: De HTML & CSS

Voordat we bij de ster van de show kunnen komen, hebben we natuurlijk wat markeringen nodig om mee te werken. Ik zal hier niet veel tijd doorbrengen, want het is niet het belangrijkste punt van deze tutorial:

    Twitter Avatar scrollen    

Dit is iets dat de twitterpersoon te zeggen had.

Dit is iets dat de twitterpersoon te zeggen had.

Dit is iets dat de twitterpersoon te zeggen had.

Dit is iets dat de twitterpersoon te zeggen had.

Dit is iets dat de twitterpersoon te zeggen had.

Dit is iets dat de twitterpersoon te zeggen had.

Dit is iets dat de twitterpersoon te zeggen had.

Ja, het is groot.

Hoe zit het met wat CSS? Probeer dit:

lichaam font: 13px / 1.5 "helvetica neue", helvetica, arial, san-serif;  artikel display: block; achtergrond: #ececec; width: 380px; padding: 10px; margin: 10px; overloop verborgen;  artikel img float: left;  artikel p marge: 0; padding-left: 60px; 

Vrij minimaal, maar het zal ons geven wat we nodig hebben.

Nu, op naar de JavaScript!


Stap 3: JavaScript, ronde 1

Ik denk dat het redelijk is om te zeggen dat dit niet je gemiddelde JavaScript-widgetwerk is; het is een beetje ingewikkelder. Hier zijn slechts een paar dingen waar u rekening mee moet houden:

  • U moet elke afbeelding verbergen die hetzelfde is als de afbeelding in de vorige? Tweet.?
  • Wanneer de pagina wordt gescrolled, moet u bepalen welke? Tweet? staat het dichtst bij de bovenkant van de pagina.
  • Als de? Tweet? is de eerste in een reeks van? tweets? door dezelfde persoon moeten we de avatar op zijn plaats fixen, zodat deze niet met de rest van de pagina zal scrollen.
  • Wanneer de top? Tweet? is de laatste in een reeks tweets door één gebruiker, we moeten de avatar op de juiste plek stoppen.
  • Dit alles moet werken om zowel naar beneden als naar boven op de pagina te scrollen.
  • Omdat dit allemaal wordt uitgevoerd elke keer dat een scroll-gebeurtenis wordt geactiveerd, het moet ongelooflijk snel zijn.

Wanneer je begint met het schrijven van iets, maak je je zorgen om het alleen maar te laten werken; optimaliseren kan later komen. Versie één negeerde verschillende belangrijke praktische tips van jQuery. Waar we hier beginnen is versie twee: de geoptimaliseerde jQuery-code.

Ik besloot dit als een jQuery-plug-in te schrijven, dus de eerste stap is om te beslissen hoe het zal worden aangeroepen; Ik ging met dit:

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

Het jQuery-object dat we de plug-in noemen, verpakt de? Tweets? (of waar je ook doorheen scrolt). De eerste parameter die de plug-in neemt, is een selector voor de elementen die zullen scrollen: de? Tweets.? De tweede selector is voor de elementen die indien nodig op hun plaats blijven (de plug-in verwacht dat dit afbeeldingen zijn, maar het zou niet veel aanpassingen vergen om het voor andere elementen te laten werken). Dus voor de HTML die we hierboven hadden, noemen we de plug-in als volgt:

$ ("sectie"). twitter_scroll ("article", ".avatar"); // OF $ ("sectie"). Twitter_scroll (". Avatar");

Zoals u zult zien wanneer we bij de code komen, is de eerste parameter optioneel; als er maar één parameter is, nemen we aan dat het de niet-selecteerbare selector is en de invoer de directe ouders van de unscrollables (ik weet het, unscrollables is een slechte naam, maar het is het meest generieke wat ik zou kunnen bedenken).

Dus, hier is onze plug-in shell:

(functie ($) jQuery.fn.twitter_scroll = function (entries, uncrollable) ; (jQuery));

Vanaf nu gaat alle JavaScript die we bekijken hierheen.

Plug-in set-up

Laten we beginnen met de setup-code; er is wat werk aan de winkel voordat we de scroll-handler instellen.

if (! unscrollable) unscrollable = $ (entries); entries = unscrollable.parent ();  else unscrollable = $ (unscrollable); entries = $ (entries); 

Ten eerste, de parameters: Als unscrollable is een false-y-waarde, dan stellen we deze in op? jQuerified? entries selector en stel in entries aan de ouders van unscrollable. Anders zullen we? JQuerify? beide parameters. Het is belangrijk om op te merken dat nu (als de gebruiker zijn markup correct heeft gedaan, wat we zullen moeten aannemen dat ze die hebben), we twee jQuery-objecten hebben waarin de overeenkomende items dezelfde index hebben: dus unscrollable [i] is het kind van entries [i]. Dit zal later nuttig zijn. (Notitie: als we niet wilden aannemen dat de gebruiker zijn document correct had gemarkeerd, of dat ze selectors gebruikten om elementen vast te leggen buiten wat we wilden, zouden we kunnen gebruiken deze als de contextparameter, of gebruik de vind methode van deze.)

Laten we vervolgens een aantal variabelen instellen; normaal zou ik dit bovenaan doen, maar er zijn er meerdere afhankelijk van unscrollable en entries, dus dat hebben we eerst aangepakt:

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 = $ (window ), scroll_top = win.scrollTop ();

Laten we deze doornemen. Zoals je je misschien kunt voorstellen, is de offset van de elementen waarmee we samenwerken belangrijk; jQuery compenseren methode retourneert een object met top en links offset eigenschappen. Voor het bovenliggende element (deze in de plug-in) en de entries, we hebben alleen de bovenste offset nodig, dus we zullen dat krijgen. Voor de unscrollables hebben we zowel bovenaan als links nodig, dus we zullen hier niets specifieks krijgen.

De variabelen prev_item, beginner, en anti_repeater wordt later gebruikt, buiten de scrolhandler of in de scrol-handler, waar we waarden nodig hebben die blijven bestaan ​​in de roepingen van de handler. Tenslotte, winnen zal op een paar plaatsen worden gebruikt, en scroll_top is de afstand van de schuifbalk vanaf de bovenkant van het venster; we gebruiken dit later om te bepalen in welke richting we scrollen.

Vervolgens gaan we bepalen welke elementen de eerste en de laatste zijn in strepen van? Tweets.? Er zijn waarschijnlijk een aantal manieren om dit te doen; we gaan dit doen door een HTML5-dataattribuut toe te passen op de juiste elementen.

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

We gebruiken jQuery's elk methode op de entries voorwerp. Onthoud dat we binnenin de functie passeren, deze verwijst naar het huidige element van entries. We hebben ook de indexparameter die we zullen gebruiken. We beginnen met het ophalen van het overeenkomstige element in de unscrollable object en bewaar het in img. Als onze invoer dat element bevat (dit zou moeten, maar we controleren het alleen), zullen we controleren of de bron van de afbeelding hetzelfde is als prev_item; als dat zo is, weten we dat deze afbeelding hetzelfde is als die in het vorige item. Daarom zullen we de afbeelding verbergen; we kunnen de display-eigenschap niet gebruiken, omdat die deze uit de stroom van het document zou verwijderen; we willen niet dat andere elementen op ons afkomen.

Dan, als beginner is waar, we geven de invoer voor dit is het attribuut data-8088-starter; onthoud dat alle HTML5-gegevenskenmerken moeten beginnen met? data- ?; en het is een goed idee om uw eigen voorvoegsel toe te voegen, zodat u niet in conflict komt met de code van andere ontwikkelaars (mijn voorvoegsel is 8088, u zult uw eigen voorvoegsel moeten vinden :)). HTML5-gegevenskenmerken moeten strings zijn, maar in ons geval zijn het niet de gegevens waar we ons zorgen over maken; we moeten alleen dat element markeren. Toen gingen we zitten beginner naar false. Tot slot, als dit het laatste item is, markeren we het als een einde.

Als de bron van de afbeelding niet hetzelfde is als de bron van de vorige afbeelding, worden we opnieuw ingesteld prev_item met de bron van uw huidige afbeelding; dan zetten we de starter op waar. Op die manier kunnen we, als we vinden dat de volgende afbeelding dezelfde bron heeft als deze, deze markeren als de starter. Tenslotte, als er een item voor deze is en de bijbehorende afbeelding is verborgen, weten we dat invoer het einde van een reeks is (omdat deze een andere afbeelding heeft). Daarom geven we het een gegevenskenmerk dat het als een einde markeert.

Als we klaar zijn, gaan we verder prev_item naar nul; we zullen het binnenkort opnieuw gaan gebruiken.

Als u nu kijkt naar de vermeldingen in Firebug, zou u zoiets als dit moeten zien:

De schuifhandler

Nu zijn we klaar om aan die schuifhandler te werken. Er zijn twee delen aan dit probleem. Eerst vinden we het item dat zich het dichtst bij de bovenkant van de pagina bevindt. Ten tweede doen we wat geschikt is voor de afbeelding met betrekking tot die vermelding.

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

Dit is onze rolbehandelingsshell; we hebben een aantal variabelen waarmee we beginnen; let nu op naar beneden. Dit zal waar zijn als we naar beneden scrollen, onwaar als we naar boven scrollen. Vervolgens worden we opnieuw ingesteld scroll_top om de afstand te zijn van de top waar we naar toe scrollen.

Laten we nu beginnen top_item om de invoer te zijn die zich het dichtst bij de bovenkant van de pagina bevindt:

top_item = entries.filter (functie (i) var distance = $ (this) .offset (). top - scroll_top; return (afstand < (parent_from_top + margin) && distance > (parent_from_top - margin)); );

Dit is helemaal niet moeilijk; we gebruiken gewoon de filter methode om te beslissen aan welk item we willen toewijzen top_item. Eerst krijgen we de afstand door het bedrag dat we hebben gescrolld af te trekken van de bovenste offset van het item. Dan keren we terug waar als afstand is tussen parent_from_top + marge en parent_from_top - margin; anders, false. Als dit je in verwarring brengt, denk er dan op deze manier over: we willen terugkeren naar waar wanneer een element zich recht bovenin het venster bevindt; in dit geval, afstand zou gelijk zijn aan 0. We moeten echter rekening houden met de bovenste offset van de container die onze? tweets bevat? dus we willen het echt afstand gelijk maken parent_from_top.

Maar het is mogelijk dat onze scrol-handler niet vuurt wanneer we precies op die pixel staan, maar in plaats daarvan wanneer we er dichtbij zijn. Ik ontdekte dit toen ik de afstand registreerde en vond dat het waarden waren die een halve pixel uit waren; ook, als je scrolafhandelingsfunctie niet al te efficiënt is (wat deze niet zal zijn, nog niet), is het mogelijk dat er niet op wordt gebrand elk scrol event. Om ervoor te zorgen dat u er een in het juiste gebied krijgt, voegen we een marge toe of trekken deze af om ons een klein bereik te geven om binnen te werken.

Nu we het bovenste item hebben, laten we er iets mee doen.

if (top_item) if (top_item.attr ("data-8088-starter")) if (! anti_repeater) child = top_item.children (unscrollable.selector); anti_repeater = child.clone (). appendTo (document.body); child.css ("visibility", "hidden"); anti_repeater.css ('positie': 'fixed', 'top': img_offset.top + 'px', 'left': img_offset.left + "px");  else if (top_item.attr ("data-8088-ender")) top_item.children (unscrollable.selector) .css ("visibility", "visible"); if (anti_repeater) anti_repeater.remove ();  anti_repeater = null;  if (! down) if (top_item.attr ("data-8088-starter")) top_item.children (unscrollable.selector) .css ("visibility", "visible"); if (anti_repeater) anti_repeater.remove ();  anti_repeater = null;  else if (top_item.attr ("data-8088-ender")) child = top_item.children (unscrollable.selector); anti_repeater = child.clone (). appendTo (document.body); child.css ("visibility", "hidden"); anti_repeater.css ('positie': 'fixed', 'top': img_offset.top + 'px', 'left': img_offset.left + "px"); 

U zult merken dat de bovenstaande code bijna tweemaal hetzelfde is; onthoud dat dit de niet-geoptimaliseerde versie is. Terwijl ik het probleem langzaamaan doorhad, begon ik met het uitzoeken hoe ik naar beneden kon scrollen; toen ik dat eenmaal had opgelost, werkte ik eraan om omhoog te scrollen. Het was niet meteen duidelijk, totdat alle functionaliteit aanwezig was, dat er zoveel gelijkenis zou zijn. Vergeet niet dat we snel zullen optimaliseren.

Dus laten we dit ontleden. Als het bovenste item een ​​heeft data-8088-starter attribuut, laten we dan kijken of het anti_repeater is vastgesteld; dit is de variabele die naar het afbeeldingselement wijst dat zal worden gerepareerd terwijl de pagina scrolt. Als anti_repeater niet is ingesteld, dan zullen we het kind van ons krijgen top_item invoer met dezelfde selector als unscrollable (nee, dit is geen slimme manier om dit te doen, we zullen het later verbeteren). Dan kloonen we dat en voegen we het toe aan het lichaam. We zullen die verstoppen en de gekloonde precies positioneren waar hij moet gaan.

Als het element geen a heeft data-8088-starter kenmerk, we zullen controleren op een data-8088-ender attribuut. Als dat er is, vinden we het juiste kind en maken het zichtbaar en verwijderen we het anti_repeater en stel die variabele in op nul.

Gelukkig, als we niet naar beneden gaan (als we omhoog gaan), is het precies het tegenovergestelde van onze twee attributen. En, als het top_item heeft geen van beide attributen, we zitten ergens in het midden van een streep en hoeven niets te veranderen.

Prestatiebeoordeling

Welnu, deze code doet wat we willen; als je het echter eens probeert, zul je merken dat je moet scrollen erg traag om het goed te laten werken. Probeer de regels toe te voegen console.profile ( "scroll") en console.profileEnd () als de eerste en laatste regels van de functie voor scrollen. Voor mij duurt de handler tussen de 2.5ms - 4ms, met 166 - 170 functieaanroepen die plaatsvinden.

Dat is veel te lang voor een scroll-handler om te rennen; en, zoals je je misschien kunt voorstellen, ik bestuur dit op een redelijk goed uitgeruste computer. Merk op dat sommige functies 30-31 keer worden genoemd; we hebben 30 vermeldingen waarmee we werken, dus dit is waarschijnlijk een onderdeel van het doorlopen van alle gegevens om de beste te vinden. Dit betekent dat hoe meer items we hebben, hoe langzamer dit gaat lopen; zo inefficiënt! Dus we moeten zien hoe we dit kunnen verbeteren.


Stap 4: Het JavaScript, ronde 2

Als je vermoedt dat jQuery hier de hoofdschuldige is, heb je gelijk. Hoewel frameworks als jQuery ontzettend handig zijn en het werken met de DOM een fluitje van een cent maken, komen ze met een compromis: performance. Ja, ze worden altijd beter; en ja, dat geldt ook voor de browsers. Echter, onze situatie vraagt ​​om de snelst mogelijke code, en in ons geval zullen we een beetje jQuery moeten opgeven voor een aantal directe DOM-werkzaamheden die niet al te veel moeilijker zijn.

De schuifhandler

Laten we met het voor de hand liggende deel: wat we doen met de top_item zodra we het hebben gevonden. Momenteel, top_item is een jQuery-object; echter, alles wat we doen met jQuery met top_item is trivially moeilijker zonder jQuery, dus we doen het? raw.? Wanneer we het verkrijgen van beoordelen top_item, we zorgen ervoor dat het een onbewerkt DOM-element is.

Dus, hier is wat we kunnen veranderen om het sneller te maken:

  • We kunnen onze if-statements refactoren om de enorme hoeveelheid duplicatie te vermijden (dit is meer een code reinheidspunt, geen prestatiepunt).
  • We kunnen de native gebruiken getAttribute methode, in plaats van jQuery's attr.
  • We kunnen het element halen unscrollable dat komt overeen met de top_item invoer, in plaats van gebruiken unscrollable.selector.
  • We kunnen de native gebruiken clodeNode en appendChild methoden, in plaats van de jQuery-versies.
  • We kunnen de gebruiken stijl eigenschap in plaats van jQuery's css methode.
  • We kunnen de native gebruiken removeNode in plaats van jQuery's verwijderen.

Door deze ideeën toe te passen, eindigen we hiermee:

if (top_item) if ((down && top_item.getAttribute ("data-8088-starter")) || (! down && top_item.getAttribute ("data-8088-ender"))) if (! anti_repeater)  child = unscrollable [entries.indexOf (top_item)]; anti_repeater = child.cloneNode (false); document.body.appendChild (anti_repeater); child.style.visibility = "verborgen"; style = anti_repeater.style; style.position = 'vast'; style.top = img_offset.top + 'px'; style.left = img_offset.left + 'px';  if ((down && top_item.getAttribute ("data-8088-ender")) || (! down && top_item.getAttribute ("data-8088-starter"))) unscrollable [entries.indexOf (top_item)] .style.visibility = "zichtbaar"; if (anti_repeater) anti_repeater.parentNode.removeChild (anti_repeater);  anti_repeater = null; 

Dit is een veel betere code: niet alleen wordt die verdubbeling verwijderd, maar er wordt ook absoluut geen jQuery gebruikt. (Terwijl ik dit schrijf, denk ik dat het misschien zelfs een beetje sneller is om een ​​CSS-klasse toe te passen om de styling te doen, daar kun je mee experimenteren!)

Je zou denken dat dat zo goed is als we kunnen krijgen; er is echter een serieuze optimalisatie die kan plaatsvinden bij het verkrijgen van top_item. Momenteel gebruiken we jQuery's filter methode. Als je erover nadenkt, is dit ongelooflijk slecht. We weten dat we slechts één item van deze filtering terug krijgen; maar de filter functie weet dat niet, dus het blijft elementen door onze functie rennen nadat we degene hebben gevonden die we willen. We hebben 30 elementen in entries, dus dat is een behoorlijk grote verspilling van tijd. Wat we willen doen is dit:

for (i = 0; entries [i]; i ++) distance = $ (entries [i]). offset (). top - scroll_top; als (afstand < (parent_from_top + margin) && distance > (parent_from_top - margin)) top_item = entries [i]; breken; 

(Afwisselend kunnen we een while-lus gebruiken, met de voorwaarde !top_item; hoe dan ook, het maakt niet veel uit.)

Op deze manier, zodra we de top_item, we kunnen stoppen met zoeken. We kunnen het echter beter; omdat scrollen lineair is, kunnen we voorspellen welk item het dichtst bij de volgende staat. Als we naar beneden scrollen, is het hetzelfde item als de vorige keer of het volgende item. Als we naar boven scrollen, is dit hetzelfde item als de vorige keer of het item ervoor. Dit zal handig zijn naarmate de pagina verder wordt geopend, omdat de for-lus altijd bovenaan de pagina begint.

Dus hoe kunnen we dit implementeren? Nou, we moeten beginnen met het bijhouden van wat de top_item was tijdens het vorige gebruik van de scroll-handler. We kunnen dit doen door toe te voegen

prev_item = top_item;

helemaal aan het einde van de scroll-verwerkingsfunctie. Nu, waar we eerder het waren top_item, zet dit:

if (prev_item) prev_item = $ (prev_item); hoogte = hoogte || prev_item.outerHeight (); if (down && prev_item.offset (). top - scroll_top + height < 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 = entries [entries.indexOf (prev_item [0]) - 1];  else top_item = prev_item [0];  else for (i = 0; entries [i]; i ++) distance = $ (entries [i]). offset (). top - scroll_top; als (afstand < (parent_from_top + margin) && distance > (parent_from_top - margin)) top_item = entries [i]; breken; 

Dit is absoluut een verbetering; als er een was prev_item, dan kunnen we dat gebruiken om het volgende te vinden top_item. De wiskunde wordt hier een beetje lastig, maar dit is wat we aan het doen zijn:

  • Als we naar beneden gaan en het vorige item volledig boven de bovenkant van de pagina staat, haal dan het volgende element op (we kunnen de index gebruiken van prev_item + 1 om het juiste element te vinden in entries).
  • Als we naar boven gaan en het vorige item ver genoeg op de pagina staat, haal je het vorige item.
  • Gebruik anders hetzelfde item als de vorige keer.
  • Als er geen is top_item, we gebruiken onze for-lus als een fallback.

Er zijn nog een paar andere zaken die hier moeten worden aangepakt. Ten eerste, wat is dit hoogte variabele? Welnu, als al onze ingaven dezelfde hoogte hebben, kunnen we voorkomen dat we de hoogte elke keer binnen de scroll-handler berekenen door het in de setup te doen. Dus ik heb dit aan de setup toegevoegd:

height = entries.outerHeight (); if (entries.length! == entries.filter (function () return $ (this) .outerHeight () === height;). length) height = null; 

jQuery outerHeight methode krijgt de hoogte van een element, inclusief opvulling en randen. Als alle elementen dezelfde hoogte hebben als de eerste, dan hoogte zal worden ingesteld; anders- hoogte zal nul zijn. Dan, wanneer het wordt top_item, we kunnen gebruiken hoogte als het is ingesteld.

Het andere ding om op te merken is dit: je zou kunnen denken dat het inefficiënt is om te doen prev_item.offset (). bovenkant tweemaal; zou dat niet binnen een variabele moeten gaan. Eigenlijk doen we het maar één keer, omdat de tweede if-verklaring alleen wordt aangeroepen als naar beneden is fout; omdat we de logische operator AND gebruiken, worden de tweede delen van de twee if-instructies nooit beide op dezelfde functieaanroep uitgevoerd. Je zou het natuurlijk in een variabele kunnen stoppen als je denkt dat het je code schoner houdt, maar het doet niets voor de prestaties.

Ik ben echter nog steeds niet tevreden. Natuurlijk, onze scrolhandler is nu sneller, maar we kunnen het beter maken. We gebruiken jQuery alleen voor twee dingen: om de outerHeight van prev_item en om de bovenste offset van te krijgen prev_item. Voor zoiets kleins is het vrij duur om een ​​jQuery-object te maken. Er is echter geen onmiddellijk-voor de hand liggende, niet-jQuery-oplossing, zoals er eerder was. Dus, laten we een duik nemen in de jQuery-broncode om precies te zien wat jQuery aan het doen is; op deze manier kunnen we de bits eruit halen die we nodig hebben zonder het dure gewicht van jQuery zelf te gebruiken.

Laten we beginnen met het probleem van de topoffset. Hier is de jQuery-code voor de compenseren methode:

functie (opties) var elem = this [0]; if (! elem ||! elem.ownerDocument) return null;  if (options) return this.each (function (i) jQuery.offset.setOffset (this, options, i););  if (elem === elem.ownerDocument.body) return 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, top = 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; return top: top, left: left; 

Dit ziet er ingewikkeld uit, maar het is niet slecht; we hebben alleen de bovenste offset nodig, niet de linker offset, dus we kunnen deze eruit halen en gebruiken om onze eigen functie te creëren:

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

Alles wat we moeten doen, gaat door in het onbewerkte DOM-element, en we krijgen de topcompensatie terug; Super goed! zodat we al onze kunnen vervangen offset (). bovenkant oproepen met deze functie.

Hoe zit het met outerHeight? Deze is een beetje lastiger omdat het een van die multi-use interne jQuery-functies gebruikt.

functie (marge) deze [0] retourneren? jQuery.css (dit [0], type, false, margin? "margin": "border"): null; 

Zoals we kunnen zien, belt dit eigenlijk gewoon naar de css functie, met deze parameters: het element,? height ?,? border ?, en vals. Dit is hoe dat eruit ziet:

functie (elem, naam, force, extra) if (naam === "width" || name === "height") var val, props = cssShow, which = name === "width"? cssWidth: cssHeight; functie getWH () val = naam === "breedte"? elem.offsetWidth: elem.offsetHeight; if (extra === "border") ga terug;  jQuery.each (welke functie () if (! extra) val - = parseFloat (jQuery.curCSS (elem, "padding" + this, true)) || 0; if (extra === "marge ") val + = parseFloat (jQuery.curCSS (elem," margin "+ this, true)) || 0; else val - = parseFloat (jQuery.curCSS (elem," border "+ this +" Width " , waar)) || 0;);  if (elem.offsetWidth! == 0) getWH ();  else jQuery.swap (elem, props, getWH);  retourneer Math.max (0, Math.round (val));  retourneer jQuery.curCSS (elem, name, force); 

We lijken op dit moment misschien hopeloos verloren, maar het is de moeite waard om de logica toch te volgen? omdat de oplossing geweldig is! Blijkbaar kunnen we alleen de elementen gebruiken offsetHeight eigendom; dat geeft ons precies wat we willen!

Daarom ziet onze code er nu als volgt uit:

if (prev_item) height = height || prev_item.offsetHeight; if (down && 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 = entries [entries.indexOf (prev_item) - 1];  else top_item = vorige_;  else for (i = 0; entries [i]; i ++) distance = get_top_offset (entries [i]) - scroll_top; als (afstand < (parent_from_top + margin) && distance > (parent_from_top - margin)) top_item = entries [i]; breken; 

Nu hoeft niets een jQuery-object te zijn! En als u het naar beneden haalt, zou u ver onder een milliseconde moeten zijn en slechts een paar functieaanroepen hebben.

Handige tools

Voordat we concluderen, is hier een handige tip om in jQuery rond te graven, zoals we hier deden: James Padolsey's jQuery Source Viewer gebruiken. Het is een geweldige tool die het ongelooflijk eenvoudig maakt om in de code te graven en te zien wat er aan de hand is zonder al te overweldigend te zijn. [Sid's note: nog een geweldige tool die je moet afrekenen - jQuery Deconstructer.]


Conclusie: wat we hebben behandeld

Ik hoop dat je deze tutorial leuk vond! Je hebt misschien geleerd hoe je een nogal netjes scrolgedrag kunt creëren, maar dat was niet het belangrijkste punt. Neem deze punten weg van deze tutorial:

  • Het feit dat uw code doet wat u wilt, betekent niet dat het niet schoner, sneller of op de een of andere manier beter kan.
  • jQuery - of welke uw favoriete bibli