Implementieren Sie Twitter-Scrolling ohne jQuery

jQuery ist ein großartiges Werkzeug, aber gibt es eine Zeit, in der Sie es nicht verwenden sollten? In diesem Lernprogramm werden wir untersuchen, wie Sie mit jQuery ein interessantes Bildlaufverhalten erstellen können. Anschließend wird erläutert, wie unser Projekt möglicherweise verbessert werden kann, indem Sie so viel jQuery oder Abstraktion wie möglich entfernen.


Intro: Der Plan

Manchmal kann Ihr Code noch "magischer" sein. durch subtrahieren von etwas jQuery Güte.

Sie könnten davon ausgehen, dass dies Ihr normales Do-Something-awesome-with-jQuery-Tutorial ist. Eigentlich nicht. Während Sie vielleicht am Ende einen ziemlich coolen Aufbau haben, aber ehrlich gesagt, vielleicht ebenso nutzlos, ist das nicht der Hauptpunkt, den ich Ihnen von diesem Tutorial nehmen möchte.

Wie Sie hoffentlich sehen werden, möchte ich, dass Sie lernen, die jQuery, die Sie schreiben, als normales JavaScript zu betrachten und zu erkennen, dass nichts Magisches daran ist. Manchmal kann Ihr Code noch "magischer" sein. durch subtrahieren von etwas jQuery Güte. Ich hoffe, dass Sie am Ende davon etwas besser mit JavaScript entwickeln können als zu Beginn.

Wenn das zu abstrakt klingt, betrachten Sie dies als eine Lektion in Bezug auf Leistung und Code-Refactoring? und auch als Entwickler aus der Komfortzone aussteigen.


Schritt 1: Das Projekt

Hier ist was wir bauen werden. Diese Inspiration erhielt ich von der relativ neuen App für Twitter für Mac. Wenn Sie die App haben (kostenlos), rufen Sie die Kontoseite einer Person auf. Wenn Sie nach unten scrollen, sehen Sie, dass sich links von jedem Tweet kein Avatar befindet. der Avatar für den ersten Tweet folgt? Sie, wie Sie nach unten scrollen. Wenn Sie einen Retweet treffen, werden Sie feststellen, dass der Avatar der retweeteten Person angemessen neben seinem Tweet platziert wird. Wenn dann die Tweets des Retweeters erneut beginnen, übernimmt der Avatar.

Dies ist die Funktionalität, die ich bauen wollte. Mit jQuery wäre das nicht schwer zu fassen, dachte ich. Und so fing ich an.


Schritt 2: Das HTML & CSS

Bevor wir zum Star der Show kommen können, brauchen wir natürlich Markup, mit dem wir arbeiten können. Ich werde nicht viel Zeit hier verbringen, weil es nicht der Hauptpunkt dieses Tutorials ist:

    Twitter-Avatar-Bildlauf    

Das war etwas, was die Twitter-Person zu sagen hatte.

Das war etwas, was die Twitter-Person zu sagen hatte.

Das war etwas, was die Twitter-Person zu sagen hatte.

Das war etwas, was die Twitter-Person zu sagen hatte.

Das war etwas, was die Twitter-Person zu sagen hatte.

Das war etwas, was die Twitter-Person zu sagen hatte.

Das war etwas, was die Twitter-Person zu sagen hatte.

Ja, es ist groß.

Wie wäre es mit CSS? Versuche dies:

body font: 13px / 1,5 "helvetica neue", helvetica, arial, san serif;  Artikel Anzeige: Block; Hintergrund: #ececec; Breite: 380px; Polsterung: 10px; Marge: 10px; Überlauf versteckt;  article img float: left;  Artikel p Marge: 0; Polsterung links: 60px; 

Ziemlich minimal, aber es wird uns geben, was wir brauchen.

Nun zum JavaScript!


Schritt 3: Das JavaScript in Runde 1

Ich denke, es ist fair zu sagen, dass dies kein gewöhnliches JavaScript-Widget ist. es ist etwas komplizierter. Hier sind nur einige Dinge, die Sie berücksichtigen müssen:

  • Sie müssen jedes Bild ausblenden, das mit dem Bild im vorherigen Tweet identisch ist.?
  • Wenn die Seite gescrollt wird, müssen Sie feststellen, welche? ist am oberen Rand der Seite.
  • Wenn der? Tweet? ist der erste in einer Reihe von? Tweets? von derselben Person müssen wir den Avatar fixieren, damit er nicht mit dem Rest der Seite scrollen kann.
  • Wenn die Spitze? Tweet? ist der letzte in einer Reihe von Tweets von einem Benutzer, wir müssen den Avatar an der entsprechenden Stelle stoppen.
  • Dies alles muss funktionieren, um die Seite nach unten und nach oben zu scrollen.
  • Da wird das alles ausgeführt Jedes Mal, wenn ein Scroll-Ereignis ausgelöst wird, es muss unglaublich schnell sein.

Wenn Sie anfangen, etwas zu schreiben, machen Sie sich Sorgen, ob es funktioniert. Optimierung kann später kommen. In Version 1 wurden einige wichtige Best Practices von jQuery ignoriert. Wir beginnen hier mit Version zwei: dem optimierten jQuery-Code.

Ich entschied mich dazu, dieses Plugin als jQuery-Plugin zu schreiben. Der erste Schritt besteht darin zu entscheiden, wie es aufgerufen wird. Ich ging mit:

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

Das jQuery-Objekt, in dem das Plugin aufgerufen wird, umfasst die "Tweets". (oder was auch immer Sie durchblättern). Der erste Parameter, den das Plugin nimmt, ist ein Selektor für die Elemente, die gescrollt werden sollen: die "Tweets". Die zweite Auswahl ist für die Elemente, die bei Bedarf an ihrem Platz bleiben (das Plugin erwartet, dass dies Bilder sind, aber es sollte nicht viel Anpassungen erfordern, damit es für andere Elemente funktioniert). Also, für das HTML, das wir oben hatten, nennen wir das Plugin so:

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

Wie Sie sehen werden, wenn wir zum Code gelangen, ist der erste Parameter optional. Wenn es nur einen Parameter gibt, gehen wir davon aus, dass es sich um den nicht scrollbaren Selektor handelt, und die Einträge sind die direkten Eltern der unscrollables. (Ich weiß, uncrollables ist ein schlechter Name, aber es ist das allgemeinste, was ich mir vorstellen könnte.).

Hier ist unsere Plugin-Shell:

(Funktion ($) jQuery.fn.twitter_scroll = Funktion (Einträge, nicht zu scrollen) ; (jQuery));

Ab jetzt gilt alles JavaScript, das wir uns ansehen, hier.

Plugin-Einrichtung

Beginnen wir mit dem Setup-Code. Bevor Sie den Scroll-Handler einrichten, müssen Sie noch etwas tun.

if (! nicht scrollable) unscrollable = $ (Einträge); entries = unscrollable.parent ();  else unscrollable = $ (nicht scrollierbar); Einträge = $ (Einträge); 

Zuerst die Parameter: Wenn nicht rollbar Ist ein falsch-y-Wert, setzen wir ihn auf den Wert von jQuerified? Einträge Wahlschalter und einstellen Einträge zu den Eltern von nicht rollbar. Ansonsten werden wir? JQuerify? beide Parameter. Es ist wichtig zu wissen, dass wir (wenn der Benutzer seine Markierung korrekt vorgenommen hat, was wir voraussetzen müssen), zwei jQuery-Objekte haben, in denen die übereinstimmenden Einträge den gleichen Index haben: so nicht rollbar [i] ist das Kind von Einträge [i]. Dies wird später nützlich sein. (Hinweis: Wenn wir nicht davon ausgehen wollten, dass der Benutzer sein Dokument richtig markiert hat oder dass er Selektoren verwendet, die Elemente außerhalb des von uns gewünschten Elements erfassen, könnten wir dies verwenden diese als Kontextparameter oder verwenden Sie die finden Methode von diese.)

Als Nächstes setzen wir einige Variablen. Normalerweise würde ich das ganz oben machen, aber mehrere hängen davon ab nicht rollbar und Einträge, also haben wir uns zuerst damit beschäftigt:

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

Lass uns durch diese gehen. Wie Sie sich vorstellen können, ist der Versatz der Elemente, mit denen wir hier arbeiten, wichtig. jQuery's Versatz Methode gibt ein Objekt mit zurück oben und links Offset-Eigenschaften. Für das übergeordnete Element (diese im Plugin) und die Einträge, Wir brauchen nur den oberen Versatz, also bekommen wir das. Für die Uncollable benötigen wir sowohl top als auch left, damit wir hier nichts konkretes erfahren.

Die Variablen prev_item, Anlasser, und anti_repeater wird später verwendet, entweder außerhalb des Scroll-Handlers oder innerhalb des Scroll-Handlers, wo Werte benötigt werden, die für alle Aufrufe des Handlers bestehen bleiben. Endlich, Sieg wird an einigen Stellen verwendet, und scroll_top ist der Abstand der Bildlaufleiste vom oberen Rand des Fensters; Wir werden dies später verwenden, um festzustellen, in welche Richtung wir scrollen.

Als Nächstes werden wir feststellen, welche Elemente in den "Tweets" die ersten und letzten sind. Es gibt wahrscheinlich mehrere Möglichkeiten, dies zu tun. Wir werden dies tun, indem wir ein HTML5-Datenattribut auf die entsprechenden Elemente anwenden.

entries.each (Funktion (i) var img = unscrollierbar [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;

Wir verwenden jQuery's jeder Methode auf der Einträge Objekt. Denken Sie daran, dass wir innerhalb der Funktion übergeben werden, diese bezieht sich auf das aktuelle Element von Einträge. Wir haben auch den Index-Parameter, den wir verwenden werden. Wir beginnen mit dem entsprechenden Element in der nicht rollbar Objekt und Speichern in img. Wenn unser Eintrag dieses Element enthält (sollte aber nur überprüft werden), prüfen wir, ob die Quelle des Bildes derselbe ist prev_item; Wenn dies der Fall ist, wissen wir, dass dieses Bild das gleiche ist wie das im vorherigen Eintrag. Deshalb verstecken wir das Bild. Die Anzeigeeigenschaft kann nicht verwendet werden, da dies den Text aus dem Dokumentfluss entfernen würde. Wir möchten nicht, dass andere Elemente auf uns wirken.

Dann wenn Anlasser ist wahr, wir geben den Eintrag Vor dieses ist das Attribut Daten-8088-Starter; Denken Sie daran, dass alle HTML5-Datenattribute mit "data-" beginnen müssen. und es ist eine gute Idee, ein eigenes Präfix hinzuzufügen, damit Sie nicht mit dem Code anderer Entwickler in Konflikt stehen (Mein Präfix ist 8088; Sie müssen Ihren eigenen finden :)). HTML5-Datenattribute müssen Strings sein, aber in unserem Fall sind es nicht die Daten, um die es uns geht. Wir müssen nur dieses Element markieren. Dann setzen wir uns Anlasser zu falsch Wenn dies der letzte Eintrag ist, markieren wir ihn als Ende.

Wenn die Quelle des Bildes nicht mit der Quelle des vorherigen Bildes übereinstimmt, werden wir zurückgesetzt prev_item mit der Quelle Ihres aktuellen Bildes; Dann setzen wir Starter auf wahr. Wenn wir also feststellen, dass das nächste Bild dieselbe Quelle wie dieses hat, können wir dieses Bild als Starter markieren. Wenn vor diesem Eintrag ein Eintrag vorhanden ist und das zugehörige Bild ausgeblendet ist, wissen wir, dass der Eintrag das Ende einer Reihe ist (da dieses Bild ein anderes Bild hat). Daher geben wir ihm ein Datenattribut, das es als Ende kennzeichnet.

Sobald wir damit fertig sind, setzen wir los prev_item zu Null; wir werden es bald wiederverwenden.

Wenn Sie sich nun die Einträge in Firebug ansehen, sollten Sie etwa Folgendes sehen:

Der Scroll-Handler

Jetzt können wir an diesem Scroll-Handler arbeiten. Dieses Problem besteht aus zwei Teilen. Zuerst finden wir den Eintrag, der dem oberen Rand der Seite am nächsten liegt. Zweitens tun wir, was für das mit diesem Eintrag verbundene Bild angemessen ist.

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

Dies ist unsere Scroll-Handler-Shell. Wir haben einige Variablen, die wir erstellen, um damit zu beginnen. gerade jetzt, zur Kenntnis nehmen Nieder. Dies wird wahr sein, wenn wir nach unten scrollen, und falsch, wenn wir nach oben scrollen. Dann setzen wir zurück scroll_top um die Entfernung von oben nach unten zu sein, zu der wir gescrollt wurden.

Nun lass uns gehen top_item Um der Eintrag am nächsten auf der Seite zu sein:

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

Das ist überhaupt nicht schwer; wir benutzen einfach die Filter Methode, um zu entscheiden, welchen Eintrag wir zuordnen möchten top_item. Zuerst bekommen wir die Entfernung indem wir den Betrag abziehen, den wir vom oberen Offset des Eintrags gescrollt haben. Dann kehren wir als wahr zurück Entfernung ist zwischen parent_from_top + margin und parent_from_top - Marge; ansonsten falsch. Wenn dies Sie verwirrt, denken Sie darüber nach: Wir möchten true zurückgeben, wenn sich ein Element ganz oben im Fenster befindet. in diesem Fall, Entfernung wäre gleich 0. Allerdings müssen wir den oberen Versatz des Containers berücksichtigen, der unsere "Tweets" umhüllt. also wollen wir wirklich Entfernung gleich sein parent_from_top.

Es ist jedoch möglich, dass unser Scroll-Handler nicht ausgelöst wird, wenn wir uns genau auf diesem Pixel befinden, sondern wenn wir uns diesem nähern. Ich entdeckte dies, als ich die Entfernung protokollierte, und es waren Werte, die einen halben Pixel entfernt waren. Auch wenn Ihre Scroll-Bearbeitungsfunktion nicht zu effizient ist (was diese Funktion nicht sein wird; noch nicht), ist es möglich, dass sie nicht startet jeden Ereignis scrollen. Um sicherzustellen, dass Sie im rechten Bereich einen Wert erhalten, addieren oder subtrahieren wir einen Rand, um einen kleinen Bereich für die Bearbeitung zu erhalten.

Nun, da wir das Top-Item haben, wollen wir etwas damit anfangen.

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 ("Sichtbarkeit", "versteckt"); 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 ("Sichtbarkeit", "sichtbar"); if (anti_repeater) anti_repeater.remove ();  anti_repeater = null;  if (! down) if (top_item.attr ("data-8088-starter")) top_item.children (unscrollable.selector) .css ("Sichtbarkeit", "sichtbar"); 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 ("Sichtbarkeit", "versteckt"); anti_repeater.css ('position': 'fixed', 'top': img_offset.top + 'px', 'left': img_offset.left + "px"); 

Möglicherweise stellen Sie fest, dass der obige Code zweimal das gleiche ist. Denken Sie daran, dass dies die nicht optimierte Version ist. Als ich das Problem langsam durcharbeitete, begann ich damit, herauszufinden, wie man das Scrollen nach unten zum Laufen bringt. Nachdem ich das gelöst hatte, arbeitete ich daran, nach oben zu scrollen. Es war nicht sofort offensichtlich, bis die gesamte Funktionalität vorhanden war, dass es so viel Ähnlichkeit gab. Denken Sie daran, wir werden bald optimieren.

Also lassen Sie uns das analysieren. Wenn der oberste Artikel eine Daten-8088-Starter Attribut, dann lassen Sie uns prüfen, ob die anti_repeater wurde gesetzt; Dies ist die Variable, die auf das Bildelement verweist, das beim Scrollen der Seite fixiert wird. Ob anti_repeater ist nicht festgelegt worden, dann bekommen wir das Kind von uns top_item Eintrag, der den gleichen Selektor hat wie nicht rollbar (Nein, das ist keine kluge Methode, wir werden es später verbessern). Dann klonen wir das und hängen es an den Körper an. Wir werden diesen verstecken und den geklonten genau dort positionieren, wo er hingehört.

Wenn das Element kein hat Daten-8088-Starter Attribut, werden wir nach einem suchen data-8088-ender Attribut. Ist dies der Fall, finden wir das richtige Kind, machen es sichtbar und entfernen das anti_repeater und setze diese Variable auf Null.

Glücklicherweise ist es genau umgekehrt, wenn wir nicht nach unten gehen (wenn wir nach oben gehen). Und wenn die top_item hat keine der Attribute, wir sind irgendwo mitten in einer Reihe und müssen nichts ändern.

Leistungsbeurteilung

Nun, dieser Code macht, was wir wollen; Wenn Sie es jedoch versuchen, werden Sie feststellen, dass Sie scrollen müssen sehr langsam damit es richtig funktioniert. Versuchen Sie, die Zeilen hinzuzufügen console.profile ("scroll") und console.profileEnd () als erste und letzte Zeile der Scroll-Bearbeitungsfunktion. Für mich dauert der Handler zwischen 2,5 ms - 4 ms, wobei 166 - 170 Funktionsaufrufe stattfinden.

Das ist viel zu lang für einen Scroll-Handler. Und wie Sie sich vorstellen können, führe ich dies auf einem relativ gut ausgestatteten Computer aus. Beachten Sie, dass einige Funktionen 30-31-mal aufgerufen werden. Wir haben 30 Einträge, mit denen wir arbeiten, daher ist dies wahrscheinlich ein Teil des Loops, um alle zu finden, um den ersten Eintrag zu finden. Dies bedeutet, dass je mehr Einträge vorhanden sind, desto langsamer wird dieser ausgeführt. so ineffizient! Wir müssen also sehen, wie wir das verbessern können.


Schritt 4: Das JavaScript in Runde 2

Wenn Sie vermuten, dass jQuery hier der Hauptschuldige ist, haben Sie recht. Frameworks wie jQuery sind zwar sehr hilfreich und machen die Arbeit mit dem DOM zu einem Kinderspiel, bringen jedoch einen Kompromiss mit sich: Leistung. Ja, sie werden immer besser. und ja, so sind die Browser. Unsere Situation erfordert jedoch den schnellstmöglichen Code, und in unserem Fall müssen wir ein wenig jQuery für eine direkte DOM-Arbeit aufgeben, die nicht allzu viel schwieriger ist.

Der Scroll-Handler

Kommen wir zum offensichtlichen Teil: Was machen wir mit dem top_item Sobald wir es gefunden haben. Zur Zeit, top_item ist ein jQuery-Objekt; jedoch alles, was wir mit jQuery machen top_item ist ohne jQuery trivial schwieriger, also machen wir es "roh". Wenn wir das Abgehen überprüfen top_item, Wir sorgen dafür, dass es sich um ein reines DOM-Element handelt.

Wir können also Folgendes ändern, um es schneller zu machen:

  • Wir können unsere if-Anweisungen umgestalten, um die große Menge an Duplizierungen zu vermeiden (dies ist eher ein Code-Sauberkeits-Punkt als ein Leistungspunkt)..
  • Wir können das native verwenden getAttribute Methode anstelle von jQuery attr.
  • Wir können das Element von bekommen nicht rollbar das entspricht dem top_item Eintrag, anstatt zu verwenden nicht scrollable.selector.
  • Wir können das native verwenden clodeNode und appendChild Methoden anstelle der jQuery-Versionen.
  • Wir können die verwenden Stil Eigenschaft anstelle von jQuery css Methode.
  • Wir können das native verwenden removeNode anstelle von jQuery Löschen.

Durch die Anwendung dieser Ideen enden wir damit:

if (top_item) if ((down && top_item.getAttribute ("data-8088-starter")) || (! down && top_item.getAttribute ("data-8088-ender"))) if (! anti_repeater)  child = nicht scrollierbar [entries.indexOf (top_item)]; anti_repeater = child.cloneNode (false); document.body.appendChild (anti_repeater); child.style.visibility = "versteckt"; 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"))) .style.visibility = "sichtbar"; if (anti_repeater) anti_repeater.parentNode.removeChild (anti_repeater);  anti_repeater = null; 

Dies ist ein viel besserer Code: Es wird nicht nur diese Duplizierung entfernt, sondern es wird auch absolut kein jQuery verwendet. (Während ich dies schreibe, denke ich, ist es vielleicht ein bisschen schneller, eine CSS-Klasse für das Styling anzuwenden; Sie können damit experimentieren!)

Sie denken vielleicht, dass das so gut ist, wie wir bekommen können; Es gibt jedoch einige gravierende Optimierungen, die beim Aussteigen erfolgen können top_item. Derzeit verwenden wir jQuery's Filter Methode. Wenn Sie darüber nachdenken, ist das unglaublich schlecht. Wir wissen, dass wir nur ein Element von dieser Filterung erhalten werden. aber die Filter function weiß das nicht, also führt es weiterhin Elemente durch unsere Funktion, nachdem wir das gefunden haben, das wir wollen. Wir haben 30 Elemente in Einträge, Das ist also eine ziemlich große Zeitverschwendung. Was wir wollen, ist folgendes:

for (i = 0; Einträge [i]; i ++) Abstand = $ (Einträge [i]). offset (). top - scroll_top; wenn (Entfernung) < (parent_from_top + margin) && distance > (parent_from_top - margin)) top_item = Einträge [i]; brechen; 

(Alternativ können wir eine while-Schleife mit der Bedingung verwenden !top_item; So oder so spielt es keine Rolle.)

Auf diese Weise, sobald wir die gefunden haben top_item, wir können aufhören zu suchen. Wir können es jedoch besser machen. Da das Scrollen eine lineare Sache ist, können wir vorhersagen, welches Element als Nächstes am nächsten ist. Wenn wir nach unten scrollen, handelt es sich entweder um dasselbe Element oder um das letzte Mal. Wenn wir nach oben scrollen, ist es das gleiche Element wie beim letzten Mal oder das Element davor. Dies wird von Nutzen sein, je weiter unten auf der Seite, da die for-Schleife immer am Anfang der Seite beginnt.

Wie können wir das umsetzen? Nun, wir müssen damit anfangen, den Überblick über das zu behalten top_item war während unseres vorherigen Laufs des Scroll-Handlers. Wir können dies durch Hinzufügen tun

prev_item = top_item;

bis zum Ende der Scroll-Handling-Funktion. Nun, wo wir vorher das gefunden haben top_item, Stellen Sie dies:

if (prev_item) prev_item = $ (prev_item); Höhe = Höhe || 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 für (i = 0; Einträge [i]; i ++) Abstand = $ (Einträge [i]). offset (). top - scroll_top; wenn (Entfernung) < (parent_from_top + margin) && distance > (parent_from_top - margin)) top_item = Einträge [i]; brechen; 

Dies ist definitiv eine Verbesserung. wenn es einen gab prev_item, dann können wir das nutzen, um das nächste zu finden top_item. Die Mathematik hier wird etwas knifflig, aber das machen wir:

  • Wenn wir nach unten gehen und sich das vorherige Element vollständig über dem oberen Rand der Seite befindet, erhalten Sie das nächste Element (wir können den Index von verwenden prev_item + 1, um das richtige Element in zu finden Einträge).
  • Wenn wir nach oben gehen und das vorherige Element weit genug auf der Seite ist, erhalten Sie den vorherigen Eintrag.
  • Andernfalls verwenden Sie denselben Eintrag wie beim letzten Mal.
  • Wenn es keine gibt top_item, Wir verwenden unsere for-Schleife als Rückfall.

Hier gibt es noch ein paar andere Dinge, die Sie ansprechen müssen. Erstens, was ist das? Höhe Variable? Wenn alle unsere Einträge dieselbe Höhe haben, können Sie die Berechnung der Höhe jedes Mal innerhalb des Scroll-Handlers vermeiden, indem Sie sie im Setup ausführen. Also habe ich das zum Setup hinzugefügt:

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

jQuery's outerHeight Die Methode erhält die Höhe eines Elements, einschließlich der Auffüllung und der Ränder. Wenn alle Elemente dieselbe Höhe wie das erste haben, dann Höhe wird gesetzt; Andernfalls Höhe wird null sein Dann, wenn Sie bekommen top_item, wir können benutzen Höhe wenn es eingestellt ist.

Die andere Sache, die man beachten sollte, ist folgende: Man könnte meinen, es wäre ineffizient zu tun prev_item.offset (). oben zweimal; sollte das nicht in eine Variable gehen. Eigentlich machen wir das nur einmal, weil die zweite if-Anweisung nur aufgerufen wird, wenn Nieder ist falsch; Da wir den logischen AND-Operator verwenden, werden die zweiten Teile der beiden if-Anweisungen niemals für denselben Funktionsaufruf ausgeführt. Natürlich könnten Sie es in eine Variable schreiben, wenn Sie der Meinung sind, dass dadurch der Code sauberer bleibt, was jedoch nicht zur Leistung beiträgt.

Ich bin jedoch immer noch nicht zufrieden. Klar, unser Scrollhandler ist jetzt schneller, aber wir können es besser machen. Wir verwenden jQuery nur für zwei Dinge: um die outerHeight von prev_item und um den oberen Versatz von zu erhalten prev_item. Für ein so kleines Objekt ist es ziemlich teuer, ein jQuery-Objekt zu erstellen. Es gibt jedoch keine sofort sichtbare, nicht jQuery-Lösung wie zuvor. Lassen Sie uns also in den jQuery-Quellcode eintauchen, um zu sehen, was genau jQuery tut. Auf diese Weise können wir die benötigten Bits herausziehen, ohne das teure Gewicht von jQuery selbst zu verwenden.

Beginnen wir mit dem Top-Offset-Problem. Hier ist der jQuery-Code für die Versatz Methode:

Funktion (Optionen) var elem = this [0]; if (! elem ||! elem.ownerDocument) return null;  if (Optionen) return this.each (Funktion (i) jQuery.offset.setOffset (this Optionen, 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 oben: oben, links: links; 

Das sieht kompliziert aus, ist aber nicht so schlimm; Wir brauchen nur den oberen Versatz, nicht den linken Versatz, sodass wir diesen herausziehen und verwenden können, um eine eigene Funktion zu erstellen:

Funktion 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, was wir tun müssen, wird im rohen DOM-Element übergeben, und wir bekommen den oberen Versatz zurück. großartig! so können wir alle unsere ersetzen Versatz () ruft mit dieser Funktion auf.

Was ist mit outerHeight? Dies ist etwas kniffliger, da es eine dieser mehrfach verwendbaren internen jQuery-Funktionen verwendet.

Funktion (Marge) zurückgeben [0]? jQuery.css (this [0], type, false, margin? "margin": "border"): null; 

Wie wir sehen können, ruft dies eigentlich nur den an css Funktion mit diesen Parametern: das Element, Höhe, Rand und falsch. So sieht das aus:

Funktion (elem, name, force, extra) if (name === "width" || name === "height") var val, Requisiten = cssShow, welcher = name === "width"? cssWidth: cssHeight; Funktion 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 === "margin ") val + = parseFloat (jQuery.curCSS (elem," margin "+ this, true)) || 0; else val - = parseFloat (jQuery.curCSS (elem," border "+ this +" Width ") wahr)) || 0;);  if (elem.offsetWidth! == 0) getWH ();  else jQuery.swap (elem, Requisiten, getWH);  return Math.max (0, Math.round (val));  return jQuery.curCSS (elem, name, force); 

Wir scheinen an diesem Punkt hoffnungslos verloren zu sein, aber es lohnt sich, der Logik zu folgen. denn die Lösung ist großartig! Es stellt sich heraus, dass wir einfach ein Element verwenden können offsetHeight Eigentum; das gibt uns genau das, was wir wollen!

Deshalb sieht unser Code jetzt so aus:

if (prev_item) Höhe = Höhe || 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 für (i = 0; Einträge [i]; i ++) Abstand = get_top_offset (Einträge [i]) - scroll_top; wenn (Entfernung) < (parent_from_top + margin) && distance > (parent_from_top - margin)) top_item = Einträge [i]; brechen; 

Nun muss nichts ein jQuery-Objekt sein! Und wenn Sie es herunterfahren, sollten Sie weit unter einer Millisekunde sein und nur ein paar Funktionsaufrufe haben.

Hilfreiche Tools

Bevor wir zum Schluss kommen, hier ein hilfreicher Tipp, um in jQuery herumzuklettern, wie wir es hier getan haben: Verwenden Sie James Padolseys jQuery Source Viewer. Es ist ein fantastisches Tool, das es unglaublich einfach macht, im Code herumzublättern und zu sehen, was los ist, ohne dabei überfordert zu sein. [Anmerkung von Sid: Ein weiteres großartiges Werkzeug, das Sie überprü