Implementa lo scorrimento di Twitter senza jQuery

jQuery è uno strumento fantastico, ma c'è mai un momento in cui non dovresti usarlo? In questo tutorial, vedremo come creare un interessante comportamento di scorrimento con jQuery, e quindi rivedere come il nostro progetto potrebbe essere migliorato rimuovendo il più possibile jQuery o astrazione.


Intro: The Plan

A volte il tuo codice può essere ancora più magico? sottraendo parte di quella bontà jQuery.

Potresti aspettarti che questo sia il tuo normale tutorial do-something-awesome-with-jQuery. In realtà, non lo è. Mentre potresti finire per creare un effetto piuttosto interessante ma, francamente, forse altrettanto inutile, non è questo il punto principale che voglio che tu togli da questo tutorial.

Come puoi vedere, voglio che impari a guardare il jQuery che stai scrivendo come un normale JavaScript, e renditi conto che non c'è niente di magico in questo. A volte il tuo codice può essere ancora più magico? sottraendo parte di quella bontà jQuery. Speriamo che, alla fine, sarai un po 'più bravo a sviluppare con JavaScript rispetto a quando hai iniziato.

Se questo sembra troppo astratto, considera questa lezione una lezione di performance e di refactoring del codice? e anche uscire dalla tua zona di comfort come sviluppatore.


Step 1: il progetto

Ecco cosa costruiremo. Mi sono ispirato alla relativamente nuova app per Twitter per Mac. Se hai l'app (è gratuita), vai a vedere la pagina dell'account di qualcuno. Mentre scorri verso il basso, vedrai che non hanno un avatar alla sinistra di ogni tweet; segue l'avatar per il primo tweet? tu mentre scorri verso il basso. Se incontri un retweet, vedrai che l'avatar della persona retweeted è posizionato appropriatamente accanto al suo tweet. Poi, quando i tweet del tweet iniziano di nuovo, il loro avatar prende il sopravvento.

Questa è la funzionalità che volevo costruire. Con jQuery, questo non sarebbe troppo difficile da mettere insieme, ho pensato. E così ho iniziato.


Passaggio 2: HTML e CSS

Certo, prima di poter arrivare alla star dello show, abbiamo bisogno di un po 'di markup con cui lavorare. Non passerò molto tempo qui, perché non è il punto principale di questo tutorial:

    Twitter Avatar Scrolling    

Questo è qualcosa che la persona twitter ha dovuto dire.

Questo è qualcosa che la persona twitter ha dovuto dire.

Questo è qualcosa che la persona twitter ha dovuto dire.

Questo è qualcosa che la persona twitter ha dovuto dire.

Questo è qualcosa che la persona twitter ha dovuto dire.

Questo è qualcosa che la persona twitter ha dovuto dire.

Questo è qualcosa che la persona twitter ha dovuto dire.

Sì, è grande.

Che ne dici di alcuni CSS? Prova questo:

body font: 13px / 1.5 "helvetica neue", helvetica, arial, san-serif;  article display: block; sfondo: #ececec; larghezza: 380px; padding: 10px; margin: 10px; overflow: hidden;  article img float: left;  article p margin: 0; padding-left: 60px; 

Abbastanza minimalista, ma ci darà ciò di cui abbiamo bisogno.

Ora, su JavaScript!


Passaggio 3: JavaScript, Round 1

Penso che sia giusto dire che questo non è il tuo widget di JavaScript medio; è un po 'più complicato. Qui ci sono solo un paio di cose che devi rendere conto:

  • Hai bisogno di nascondere ogni immagine uguale a quella del precedente? Tweet.?
  • Quando la pagina scorre, devi determinare quale? Tweet? è più vicino alla parte superiore della pagina.
  • Se il? Tweet? è il primo di una serie di? tweets? dalla stessa persona, dobbiamo sistemare l'avatar sul posto, quindi non scorrerà con il resto della pagina.
  • Quando il top? Tweet? è l'ultimo di una serie di tweet di un utente, dobbiamo fermare l'avatar nel punto appropriato.
  • Tutto questo deve funzionare per scorrere sia verso il basso che verso l'alto della pagina.
  • Dal momento che tutto questo viene eseguito ogni volta che si attiva un evento di scorrimento, deve essere incredibilmente veloce.

Quando inizi a scrivere qualcosa, preoccupati solo di farlo funzionare; l'ottimizzazione può venire dopo. La prima versione ignorava alcune importanti best practice di jQuery. Ciò che iniziamo qui è la seconda versione: il codice jQuery ottimizzato.

Ho deciso di scrivere questo come plugin jQuery, quindi il primo passo è decidere come verrà chiamato; Sono andato con questo:

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

L'oggetto jQuery che chiamiamo plugin, avvolge i tweet? (o qualunque cosa tu stia scorrendo). Il primo parametro che il plugin prende è un selettore per gli elementi che scorreranno: i "tweet". Il secondo selettore è per gli elementi che restano sul posto quando necessario (il plug-in si aspetta che siano immagini, ma non dovrebbe richiedere molto aggiustamento per farlo funzionare per altri elementi). Quindi, per l'HTML di cui sopra, chiamiamo il plugin in questo modo:

$ ("sezione"). twitter_scroll ("article", ".avatar"); // OR $ ("sezione"). Twitter_scroll (". Avatar");

Come vedrai quando arriveremo al codice, il primo parametro sarà opzionale; se c'è un solo parametro, supponiamo che sia il selettore non convertibile, e le voci siano i diretti genitori degli unscrollables (lo so, unscrollables è un brutto nome, ma è la cosa più generica che potrei inventare).

Quindi, ecco la nostra shell di plugin:

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

D'ora in poi, tutto il codice JavaScript che esamineremo andrà qui.

Impostazione plug-in

Iniziamo con il codice di installazione; c'è del lavoro da fare prima di impostare il gestore di scorrimento.

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

Innanzitutto, i parametri: If unscrollable è un valore falso-y, lo imposteremo sul valore? jQuerified? inserimenti selettore e imposta inserimenti ai genitori di unscrollable. Altrimenti, lo faremo? entrambi i parametri. È importante notare che ora (se l'utente ha eseguito correttamente il suo markup, che dovremo assumere che hanno), abbiamo due oggetti jQuery in cui le voci corrispondenti hanno lo stesso indice: quindi unscrollable [i] è il figlio di voci di [i]. Questo sarà utile in seguito. (Nota: se non volessimo ritenere che l'utente abbia marcato correttamente il loro documento, o che usassero selettori che catturerebbero elementi al di fuori di ciò che vogliamo, potremmo usare Questo come parametro di contesto o utilizzare il trova metodo di Questo.)

Quindi, impostiamo alcune variabili; normalmente, lo farei in alto, ma molti dipendono da unscrollable e inserimenti, quindi ci siamo occupati di questo primo:

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

Corriamo attraverso questi. Come puoi immaginare, l'offset degli elementi con cui stiamo lavorando qui è importante; jQuery di compensare metodo restituisce un oggetto con superiore e sinistra proprietà offset. Per l'elemento genitore (Questo all'interno del plugin) e il inserimenti, avremo solo bisogno dell'offset superiore, quindi lo otterremo. Per gli unscrollables, avremo bisogno sia di top che di left, quindi non otterremo nulla di specifico qui.

Le variabili prev_item, antipasto, e anti_repeater verrà utilizzato in seguito, al di fuori del gestore di scorrimento o all'interno del gestore di scorrimento, dove avremo bisogno di valori che persistono attraverso le chiamate del gestore. Finalmente, vincere sarà usato in alcuni posti, e scroll_top è la distanza della barra di scorrimento dalla parte superiore della finestra; lo useremo in seguito per determinare in quale direzione stiamo scorrendo.

Successivamente, determineremo quali elementi sono i primi e gli ultimi in serie di "tweet". Ci sono probabilmente un paio di modi per farlo; lo faremo applicando un attributo di dati HTML5 agli elementi appropriati.

entries.each (function (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 (voci [i-1] && unscrollable [i-1] .style.visibility === " hidden ") entries [i-1] .setAttribute (" data-8088-ender "," true ");); prev_item = null;

Stiamo usando jQuery ogni metodo sul inserimenti oggetto. Ricorda che all'interno della funzione passiamo, Questo si riferisce all'elemento corrente da inserimenti. Abbiamo anche il parametro index, che useremo. Inizieremo ottenendo l'elemento corrispondente nel file unscrollable oggetto e memorizzarlo img. Quindi, se la nostra voce contiene quell'elemento (dovrebbe, ma stiamo solo controllando), controlleremo se l'origine dell'immagine è la stessa di prev_item; se lo è, sappiamo che questa immagine è uguale a quella della voce precedente. Pertanto, nasconderemo l'immagine; non possiamo usare la proprietà display, perché ciò rimuoverebbe dal flusso del documento; non vogliamo che altri elementi si muovano su di noi.

Quindi, se antipasto è vero, daremo la voce prima questo è l'attributo Dati-8088-starter; ricorda che tutti gli attributi dei dati HTML5 devono iniziare con? data- ?; ed è una buona idea aggiungere il proprio prefisso in modo da non entrare in conflitto con il codice degli altri sviluppatori (Il mio prefisso è 8088, dovrete trovarne uno :)). Gli attributi dei dati HTML5 devono essere stringhe, ma nel nostro caso non sono i dati a preoccuparci; abbiamo solo bisogno di segnare quell'elemento Quindi abbiamo impostato antipasto a falso. Infine, se questa è l'ultima voce, la contrassegneremo come una fine.

Se la fonte dell'immagine non è uguale alla sorgente dell'immagine precedente, verrà ripristinata prev_item con la fonte della tua immagine attuale; quindi, iniziamo a impostare vero. In questo modo, se scopriamo che l'immagine successiva ha la stessa fonte di questa, possiamo contrassegnarla come l'iniziatore. Infine, se c'è una voce prima di questa e l'immagine associata è nascosta, sappiamo che la voce è la fine di una serie (perché questa ha un'immagine diversa). Pertanto, gli forniremo un attributo di dati che lo segna come una fine.

Una volta terminato, lo imposteremo prev_item a nullo; lo riutilizzeremo presto.

Ora, se guardi le voci in Firebug, dovresti vedere qualcosa di simile a questo:

Il gestore di scorrimento

Ora siamo pronti per lavorare su quel gestore di scroll. Ci sono due parti per questo problema. Innanzitutto, troviamo la voce più vicina alla parte superiore della pagina. In secondo luogo, facciamo tutto ciò che è appropriato per l'immagine relativa a quella voce.

$ (document) .bind ("scroll", function (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 );

Questa è la shell del nostro gestore di scroll; abbiamo un paio di variabili che stiamo creando per iniziare; adesso, prendi nota di giù. Questo sarà vero se stiamo scorrendo verso il basso, falso se stiamo scorrendo verso l'alto. Quindi, stiamo resettando scroll_top per essere la distanza in giù dalla cima a cui siamo passati.

Ora, impostiamo top_item per essere la voce più vicina all'inizio della pagina:

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

Questo non è affatto difficile; usiamo solo il filtro metodo per decidere quale voce vogliamo assegnare top_item. In primo luogo, otteniamo il distanza sottraendo l'importo che abbiamo scostato dall'offset superiore della voce. Quindi, restituiamo true se distanza è tra parent_from_top + margine e parent_from_top - margine; altrimenti, falso. Se questo ti confonde, pensaci in questo modo: vogliamo restituire true quando un elemento è proprio in cima alla finestra; in questo caso, distanza sarebbe uguale a 0. Tuttavia, dobbiamo tenere conto dell'offset superiore del contenitore che avvolge i nostri? tweets? quindi vogliamo davvero distanza uguale parent_from_top.

Ma è possibile che il nostro gestore di scorrimento non si attivi quando siamo esattamente su quel pixel, ma invece quando siamo vicini ad esso. L'ho scoperto quando ho registrato la distanza e ho scoperto che erano valori a metà pixel; inoltre, se la tua funzione di gestione dello scorrimento non è troppo efficiente (che questo non sarà, non ancora), è possibile che non si accenda ogni scorrere l'evento. Per essere sicuri di averne uno nella giusta area, aggiungiamo o sottraggiamo un margine per darci un piccolo raggio di lavoro all'interno.

Ora che abbiamo il primo oggetto, facciamo qualcosa con esso.

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 ('position': '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 ('position': 'fixed', 'top': img_offset.top + 'px', 'left': img_offset.left + "px"); 

Potresti notare che il codice sopra riportato è quasi la stessa cosa due volte; ricorda che questa è la versione non ottimizzata. Come ho lentamente risolto il problema, ho iniziato a capire come andare a scorrere verso il basso lavorando; una volta risolto ciò, ho lavorato su per scorrere. Non era immediatamente ovvio, fino a quando tutta la funzionalità era a posto, che ci sarebbe stata molta somiglianza. Ricorda, ottimizzeremo presto.

Quindi, analizziamo questo. Se l'elemento in alto ha un Dati-8088-starter attributo, quindi, controlliamo per vedere se il anti_repeater è stato impostato; questa è la variabile che punterà all'elemento dell'immagine che verrà corretto mentre la pagina scorre. Se anti_repeater non è stato impostato, quindi avremo il bambino il nostro di top_item voce che ha lo stesso selettore di unscrollable (no, questo non è un modo intelligente per farlo, lo miglioreremo in seguito). Quindi, lo cloniamo e lo aggiungiamo al corpo. Lo nasconderemo e quindi posizioneremo quello clonato esattamente dove dovrebbe andare.

Se l'elemento non ha a Dati-8088-starter attributo, controlleremo per a Dati-8088-ender attributo. Se è lì, troveremo il bambino giusto e renderlo visibile, quindi rimuovere il anti_repeater e imposta quella variabile su nullo.

Fortunatamente, se non stiamo andando giù (se stiamo salendo), è esattamente il contrario per i nostri due attributi. E, se il top_item non ha attributi, siamo nel bel mezzo di una serie e non dobbiamo cambiare nulla.

Valutazione delle prestazioni

Bene, questo codice fa quello che vogliamo; tuttavia, se ci provi, noterai che devi scorrere molto lentamente perché funzioni correttamente Prova ad aggiungere le linee console.profile ( "scroll") e console.profileEnd () come prima e ultima riga della funzione di gestione dello scorrimento. Per me, il gestore prende tra 2,5 ms - 4 ms, con 166 - 170 chiamate di funzione in corso.

È troppo lungo per l'esecuzione di un gestore di scorrimento; e, come puoi immaginare, sto eseguendo questo su un computer abbastanza ben dotato. Si noti che alcune funzioni vengono chiamate 30-31 volte; abbiamo 30 voci con cui stiamo lavorando, quindi questo probabilmente fa parte del ciclo su tutti loro per trovare quello in alto. Ciò significa che più voci abbiamo, più lento sarà eseguito; così inefficiente! Quindi, dobbiamo vedere come possiamo migliorare questo.


Passaggio 4: JavaScript, Round 2

Se sospetti che jQuery sia il colpevole principale qui, hai ragione. Mentre i framework come jQuery sono incredibilmente utili e rendono il lavoro con il DOM un gioco da ragazzi, hanno un compromesso: le prestazioni. Sì, stanno sempre migliorando; e sì, lo sono anche i browser. Tuttavia, la nostra situazione richiede il codice più veloce possibile e, nel nostro caso, dovremo rinunciare a un po 'di jQuery per alcuni lavori DOM diretti che non sono molto più difficili.

Il gestore di scorrimento

Facciamo la parte ovvia: cosa facciamo con il top_item una volta che l'abbiamo trovato. Attualmente, top_item è un oggetto jQuery; tuttavia, tutto ciò che stiamo facendo con jQuery con top_item è banalmente più difficile senza jQuery, quindi lo faremo? Quando esaminiamo l'acquisizione di top_item, ci assicureremo che sia un elemento DOM grezzo.

Quindi, ecco cosa possiamo cambiare per renderlo più veloce:

  • Possiamo refactoring le nostre dichiarazioni if ​​per evitare l'enorme quantità di duplicati (questo è più un punto di pulizia del codice, non un punto di prestazione).
  • Possiamo usare il nativo getAttribute metodo, invece di jQuery attr.
  • Possiamo ottenere l'elemento da unscrollable quello corrisponde al top_item entrata, invece di usare unscrollable.selector.
  • Possiamo usare il nativo clodeNode e appendChild metodi, invece delle versioni di jQuery.
  • Possiamo usare il stile proprietà invece di jQuery css metodo.
  • Possiamo usare il nativo removeNode invece di jQuery rimuovere.

Applicando queste idee, finiamo con questo:

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 = "hidden"; style = anti_repeater.style; style.position = 'fixed'; 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 = "visible"; if (anti_repeater) anti_repeater.parentNode.removeChild (anti_repeater);  anti_repeater = null; 

Questo è un codice molto migliore: non solo si sbarazza di quella duplicazione, ma usa anche assolutamente jQuery. (Mentre sto scrivendo questo, penso che potrebbe essere anche un po 'più veloce applicare una classe CSS per lo styling: puoi sperimentare con quello!)

Potresti pensare che sia il massimo che possiamo ottenere; tuttavia, c'è un po 'di ottimizzazione seria che può aver luogo nel processo di acquisizione top_item. Attualmente stiamo usando jQuery filtro metodo. Se ci pensi, questo è incredibilmente povero. Sappiamo che recupereremo solo un elemento da questo filtro; ma il filtro la funzione non lo sa, quindi continua a eseguire elementi attraverso la nostra funzione dopo aver trovato quello che vogliamo. Abbiamo 30 elementi in inserimenti, quindi è una perdita enorme di tempo. Quello che vogliamo fare è questo:

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

(In alternativa, potremmo usare un ciclo while, con la condizione !top_item; in entrambi i casi, non importa molto.)

In questo modo, una volta trovato il top_item, possiamo smettere di cercare. Tuttavia, possiamo fare di meglio; poiché lo scorrimento è una cosa lineare, possiamo prevedere quale elemento sarà più vicino alla cima successiva. Se stiamo scorrendo verso il basso, sarà lo stesso oggetto dell'ultima volta o quello successivo. Se stiamo scorrendo, sarà lo stesso oggetto dell'ultima volta o l'elemento precedente. Ciò sarà utile più in basso nella pagina che otteniamo, perché il ciclo for inizia sempre nella parte superiore della pagina.

Quindi, come possiamo implementarlo? Bene, dobbiamo iniziare tenendo traccia di cosa top_item era durante la nostra precedente esecuzione del gestore di scorrimento. Possiamo farlo aggiungendo

prev_item = top_item;

fino alla fine della funzione di gestione dello scorrimento. Ora, su dove stavamo trovando in precedenza top_item, metti questo:

if (prev_item) prev_item = $ (prev_item); altezza = altezza || 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; se (distanza < (parent_from_top + margin) && distance > (parent_from_top - margin)) top_item = entries [i]; rompere; 

Questo è sicuramente un miglioramento; se ci fosse un prev_item, allora possiamo usare quello per trovare il prossimo top_item. La matematica qui diventa un po 'complicata, ma questo è quello che stiamo facendo:

  • Se stiamo andando giù e l'elemento precedente è completamente sopra la parte superiore della pagina, ottieni l'elemento successivo (possiamo usare l'indice di prev_item + 1 per trovare l'elemento giusto in inserimenti).
  • Se stiamo salendo e l'articolo precedente è abbastanza lontano dalla pagina, prendi la voce precedente.
  • Altrimenti, usa la stessa voce dell'ultima volta.
  • Se non c'è top_item, usiamo il nostro ciclo for come fallback.

Ci sono alcune altre cose da affrontare qui. Innanzitutto, cos'è questo altezza variabile? Bene, se tutte le voci hanno la stessa altezza, possiamo evitare di calcolare l'altezza ogni volta all'interno del gestore di scorrimento eseguendolo nel setup. Quindi, ho aggiunto questo al setup:

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

jQuery di outerHeight il metodo ottiene l'altezza di un elemento, inclusi i suoi padding e bordi. Se tutti gli elementi hanno la stessa altezza del primo, allora altezza sarà impostato; altrimenti altezza sarà nullo. Quindi, quando si ottiene top_item, possiamo usare altezza se è impostato.

L'altra cosa da notare è questa: potresti pensare che sia inefficiente da fare prev_item.offset (). top due volte; non dovrebbe andare all'interno di una variabile. In realtà, lo faremo solo una volta, perché la seconda istruzione if verrà chiamata solo se giù è falso; poiché stiamo utilizzando l'operatore AND logico, le seconde parti delle due istruzioni if ​​non verranno mai entrambe eseguite sulla stessa chiamata di funzione. Certo, potresti metterlo in una variabile se pensi che mantenga il tuo codice più pulito, ma non fa nulla per le prestazioni.

Tuttavia, non sono ancora soddisfatto. Certo, il nostro gestore di scroll è più veloce ora, ma possiamo renderlo migliore. Usiamo solo jQuery per due cose: ottenere il outerHeight di prev_item e ottenere l'offset superiore di prev_item. Per qualcosa di così piccolo, è piuttosto costoso creare un oggetto jQuery. Tuttavia, non esiste una soluzione immediatamente apparente, non jQuery, come prima. Quindi, tuffiamoci nel codice sorgente di jQuery per vedere esattamente cosa sta facendo jQuery; in questo modo, possiamo estrarre i bit di cui abbiamo bisogno senza utilizzare il peso costoso di jQuery stesso.

Iniziamo con il problema dell'offset superiore. Ecco il codice jQuery per il compensare metodo:

function (options) 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; 

Questo sembra complicato, ma non è male; abbiamo solo bisogno dell'offset superiore, non dell'offset sinistro, quindi possiamo estrarlo e usarlo per creare la nostra funzione:

function 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; 

Tutto ciò che dobbiamo fare è passare nell'elemento DOM grezzo e restituiremo l'offset superiore; grande! quindi possiamo sostituire tutto il nostro offset (). top chiama con questa funzione.

Che ne dici di outerHeight? Questo è un po 'più complicato, perché usa una di quelle funzioni jQuery interne multiuso.

function (margin) return this [0]? jQuery.css (questo [0], type, false, margin? "margin": "border"): null; 

Come possiamo vedere, questo in realtà fa solo una chiamata al css funzione, con questi parametri: l'elemento,? height ?,? border ?, e falso. Ecco come appare:

function (elem, name, force, extra) if (name === "width" || name === "height") var val, props = cssShow, which = name === "width"? cssWidth: cssHeight; function getWH () val = name === "width"? elem.offsetWidth: elem.offsetHeight; if (extra === "border") return;  jQuery.each (which, function () if (! extra) val - = parseFloat (jQuery.curCSS (elem, "padding" + this, true)) || 0; if (extra === "margine ") val + = parseFloat (jQuery.curCSS (elem," margin "+ this, true)) || 0; else val - = parseFloat (jQuery.curCSS (elem," border "+ this +" Width ") , vero)) || 0;);  if (elem.offsetWidth! == 0) getWH ();  else jQuery.swap (elem, props, getWH);  restituisce Math.max (0, Math.round (val));  return jQuery.curCSS (elem, name, force); 

Potremmo sembrare irrimediabilmente persi a questo punto, ma vale comunque la pena seguire la logica? perché la soluzione è fantastica! Risulta, possiamo semplicemente usare un elemento offsetHeight proprietà; questo ci dà esattamente quello che vogliamo!

Pertanto, il nostro codice ora si presenta così:

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 = prev_item;  else for (i = 0; entries [i]; i ++) distance = get_top_offset (entries [i]) - scroll_top; se (distanza < (parent_from_top + margin) && distance > (parent_from_top - margin)) top_item = entries [i]; rompere; 

Ora, niente deve essere un oggetto jQuery! E se lo esegui, dovresti essere molto meno di un millisecondo e avere solo un paio di chiamate di funzione.

Strumenti utili

Prima di concludere, ecco un suggerimento utile per scavare all'interno di jQuery come abbiamo fatto qui: usa il jQuery Source Viewer di James Padolsey. È uno strumento fantastico che rende incredibilmente semplice scavare all'interno del codice e vedere cosa succede senza essere troppo sopraffatto. [Nota di Sid: un altro fantastico strumento da provare - jQuery Deconstructer.]


Conclusione: ciò che abbiamo coperto

Spero ti sia piaciuto questo tutorial! Potresti aver imparato come creare un comportamento di scorrimento piuttosto ordinario, ma non era questo il punto principale. Dovresti prendere questi punti di distanza da questo tutorial:

  • Il fatto che il tuo codice faccia quello che vuoi non significa che non possa essere più pulito, o più veloce, o in qualche modo migliore.
  • jQuery, o qualunque sia la tua libreria preferita, è solo JavaScript; può essere anche lento; e a volte, è meglio non usarlo.
  • L'unico modo in cui migliorerai come sviluppatore JavaScript è se ti allontani dalla tua zona di comfort.

Hai qualche domanda? O forse hai un'idea su come migliorare ancora di più questo! Discutiamone nei commenti!