Es gab mehrere ausgezeichnete Artikel, die untersuchten, wie diese API genutzt werden kann, darunter Beiträge von Autoren wie Phil Hawksworth, Preethi und Mateusz Rybczonek, um nur einige zu nennen. Aber ich möchte hier etwas anders machen. Ich hatte Anfang des Jahres die Gelegenheit, die VueJS Transition-Komponente auf dem Dallas VueJS Meetup vorzustellen, auf der mein erster Artikel auf CSS-Tricks basierte. Während der Frage-und-Antwort-Runde dieser Präsentation wurde ich gefragt, wie man Übergänge basierend auf Scroll-Ereignissen auslöst – was man natürlich tun kann, aber ein Mitglied des Publikums schlug vor, sich den Intersection Observer anzusehen.
Das brachte mich zum Nachdenken. Ich kannte die Grundlagen des Intersection Observer und wie man ein einfaches Beispiel dafür erstellt. Wusste ich, wie ich erklären kann, nicht nur, wie man ihn benutzt, sondern auch, wie er funktioniert? Was genau bietet er uns Entwicklern? Wie würde ich ihn als „Senior“-Entwickler jemandem erklären, der gerade von einem Bootcamp kommt und ihn vielleicht noch nicht einmal kennt?
Ich beschloss, dass ich es wissen musste. Nachdem ich einige Zeit recherchiert, getestet und experimentiert habe, habe ich beschlossen, einen großen Teil dessen zu teilen, was ich gelernt habe. Daher sind wir hier.
Eine kurze Erklärung des Intersection Observer
Der Abstract des W3C Public Working Draft (erste Version vom 14. September 2017) beschreibt die Intersection Observer API als
Diese Spezifikation beschreibt eine API, die verwendet werden kann, um die Sichtbarkeit und Position von DOM-Elementen („Targets“) relativ zu einem enthaltenden Element oder zum obersten Viewport („Root“) zu verstehen. Die Position wird asynchron geliefert und ist nützlich für das Verständnis der Sichtbarkeit von Elementen sowie für die Implementierung von Vorab- und verzögerten Ladevorgängen von DOM-Inhalten.
Die allgemeine Idee ist eine Möglichkeit, ein untergeordnetes Element zu beobachten und informiert zu werden, wenn es in die Bounding Box eines seiner Elternteile eintritt. Dies geschieht am häufigsten im Zusammenhang mit dem Scrollen des Zielelements in den Viewport des Elternelements. Bis zur Einführung des Intersection Observer wurde diese Art von Funktionalität durch das Abhören von Scroll-Ereignissen erreicht.
Obwohl der Intersection Observer eine performantere Lösung für diese Art von Funktionalität ist, schlage ich nicht vor, dass wir ihn unbedingt als Ersatz für Scroll-Ereignisse betrachten sollten. Stattdessen schlage ich vor, diese API als zusätzliches Werkzeug zu betrachten, das eine funktionale Überschneidung mit Scroll-Ereignissen hat. In einigen Fällen können die beiden zusammenarbeiten, um spezifische Probleme zu lösen.
Ein einfaches Beispiel
Ich weiß, ich riskiere, das zu wiederholen, was bereits in anderen Artikeln erklärt wurde, aber sehen wir uns ein einfaches Beispiel für einen Intersection Observer an und was er uns bietet.
Der Observer besteht aus vier Teilen
- dem „Root“, dem Elternelement, an das der Observer gebunden ist, was der Viewport sein kann
- dem „Target“, dem zu beobachtenden untergeordneten Element, und es können auch mehrere sein
- dem Options-Objekt, das bestimmte Aspekte des Observer-Verhaltens definiert
- der Callback-Funktion, die jedes Mal aufgerufen wird, wenn eine Schnittstellenänderung beobachtet wird
Der Code eines einfachen Beispiels könnte so aussehen
const options = {
root: document.body,
rootMargin: '0px',
threshold: 0
}
function callback (entries, observer) {
console.log(observer);
entries.forEach(entry => {
console.log(entry);
});
}
let observer = new IntersectionObserver(callback, options);
observer.observe(targetElement);
Der erste Abschnitt im Code ist das Options-Objekt, das die Eigenschaften root, rootMargin und threshold enthält.
Der root ist das Elternelement, oft ein scrollendes Element, das die beobachteten Elemente enthält. Dies kann praktisch jedes einzelne Element auf der Seite sein, je nach Bedarf. Wenn die Eigenschaft überhaupt nicht angegeben ist oder der Wert auf null gesetzt ist, wird der Viewport als Root-Element festgelegt.
Der rootMargin ist eine Zeichenkette von Werten, die den Rand des Root-Elements beschreibt und die resultierende Bounding Box beeinflusst, in die das Target-Element gescrollt wird. Er verhält sich ähnlich wie die CSS-Eigenschaft margin. Sie können Werte wie 10px 15px 20px haben, was uns einen oberen Rand von 10px, linke und rechte Ränder von 15px und einen unteren Rand von 20px gibt. Nur die Bounding Box wird beeinflusst und nicht das Element selbst. Beachten Sie, dass die einzigen zulässigen Längen Pixel- und Prozentwerte sind, die negativ oder positiv sein können. Beachten Sie auch, dass rootMargin nicht funktioniert, wenn das Root-Element kein tatsächliches Element auf der Seite ist, wie z. B. der Viewport.
Der threshold ist der Wert, der bestimmt, wann eine Schnittstellenänderung beobachtet werden soll. Mehrere Werte können in einem Array enthalten sein, sodass dasselbe Target die Schnittstelle mehrmals auslösen kann. Die verschiedenen Werte sind Prozentsätze von Null bis Eins, ähnlich wie opacity in CSS. Ein Wert von 0,5 wird also als 50 % betrachtet und so weiter. Diese Werte beziehen sich auf das Schnittverhältnis des Targets, das gleich erklärt wird. Ein Schwellenwert von Null löst die Schnittstelle aus, wenn das erste Pixel des Target-Elements mit dem Root-Element kollidiert. Ein Schwellenwert von Eins löst aus, wenn das gesamte Target-Element innerhalb des Root-Elements liegt.
Der zweite Abschnitt im Code ist die Callback-Funktion, die aufgerufen wird, wann immer eine Schnittstellenänderung beobachtet wird. Zwei Parameter werden übergeben; die Einträge werden in einem Array gespeichert und repräsentieren jedes Target-Element, das die Schnittstellenänderung auslöst. Dies liefert viele Informationen, die für den Großteil der Funktionalität verwendet werden können, die ein Entwickler erstellen könnte. Der zweite Parameter sind Informationen über den Observer selbst, der im Wesentlichen die Daten aus dem bereitgestellten options-Objekt enthält. Dies bietet eine Möglichkeit zu identifizieren, welcher Observer im Einsatz ist, falls ein Target an mehrere Observer gebunden ist.
Der dritte Abschnitt im Code ist die Erstellung des Observers selbst und wo er das Target beobachtet. Bei der Erstellung des Observers können die Callback-Funktion und das options-Objekt extern zum Observer liegen, wie gezeigt. Ein Entwickler könnte den Code Inline schreiben, aber der Observer ist sehr flexibel. Zum Beispiel können die Callback-Funktion und die Optionen bei Bedarf für mehrere Observer verwendet werden. Die observe()-Methode erhält dann das zu beobachtende Target-Element. Sie kann nur ein Target gleichzeitig aufnehmen, aber die Methode kann für mehrere Targets auf demselben Observer wiederholt werden. Wiederum sehr flexibel.
Beachten Sie die Konsolenprotokolle im Code. Hier sind die Ausgaben.
Das Observer-Objekt
Das Protokollieren der an die Callback-Funktion übergebenen Observer-Daten ergibt etwas wie dieses
IntersectionObserver
root: null
rootMargin: "0px 0px 0px 0px"
thresholds: Array [ 0 ]
<prototype>: IntersectionObserverPrototype { }
…was im Wesentlichen das options-Objekt ist, das beim Erstellen an den Observer übergeben wurde. Dies kann verwendet werden, um das Root-Element zu identifizieren, an das die Schnittstelle gebunden ist. Beachten Sie, dass dieses Objekt, obwohl das ursprüngliche options-Objekt 0px für rootMargin hatte, 0px 0px 0px 0px meldet, was angesichts der Regeln für CSS-Ränder zu erwarten ist. Dann gibt es das Array von Schwellenwerten, unter denen der Observer arbeitet.
Das Entry-Objekt
Das Protokollieren der an die Callback-Funktion übergebenen Entry-Daten ergibt etwas wie dieses
IntersectionObserverEntry
boundingClientRect: DOMRect
bottom: 923.3999938964844, top: 771
height: 152.39999389648438, width: 411
left: 9, right: 420
x: 9, y: 771
<prototype>: DOMRectPrototype { }
intersectionRatio: 0
intersectionRect: DOMRect
bottom: 0, top: 0
height: 0, width: 0
left: 0, right: 0
x: 0, y: 0
<prototype>: DOMRectPrototype { }
isIntersecting: false
rootBounds: null
target: <div class="item">
time: 522
<prototype>: IntersectionObserverEntryPrototype { }
Ja, hier passiert viel.
Für die meisten Entwickler sind die beiden nützlichsten Eigenschaften intersectionRatio und isIntersecting. Die Eigenschaft isIntersecting ist ein boolescher Wert, der genau das tut, was man vermuten würde – das Target-Element überschneidet sich zum Zeitpunkt der Schnittstellenänderung mit dem Root-Element. Das intersectionRatio ist der Prozentsatz des Target-Elements, der sich gerade mit dem Root-Element überschneidet. Dies wird als Prozentsatz von Null bis Eins dargestellt, ähnlich wie der Schwellenwert im Options-Objekt des Observers.
Drei Eigenschaften – boundingClientRect, intersectionRect und rootBounds – repräsentieren spezifische Daten zu drei Aspekten der Schnittstelle. Die Eigenschaft boundingClientRect liefert die Bounding Box des Target-Elements mit den Werten unten, links, rechts und oben vom oberen linken Rand des Viewports, genau wie bei Element.getBoundingClientRect(). Dann werden Höhe und Breite des Target-Elements als X- und Y-Koordinaten angegeben. Die Eigenschaft rootBounds liefert dieselben Daten für das Root-Element. Das intersectionRect liefert ähnliche Daten, beschreibt aber die durch den Schnittbereich des Target-Elements innerhalb des Root-Elements gebildete Box, die dem Wert intersectionRatio entspricht. Traditionelle Scroll-Ereignisse würden erfordern, dass diese Berechnungen manuell durchgeführt werden.
Eine Sache, die man beachten sollte, ist, dass alle diese Formen, die die verschiedenen Elemente darstellen, immer Rechtecke sind. Unabhängig von der tatsächlichen Form der beteiligten Elemente werden sie immer auf das kleinste Rechteck reduziert, das das Element umschließt.
Die Eigenschaft target bezieht sich auf das zu beobachtende Target-Element. In Fällen, in denen ein Observer mehrere Targets enthält, ist dies der einfache Weg, um zu bestimmen, welches Target-Element diese Schnittstellenänderung ausgelöst hat.
Die Eigenschaft time liefert die Zeit (in Millisekunden) vom Zeitpunkt der Erstellung des Observers bis zum Zeitpunkt, an dem diese Schnittstellenänderung ausgelöst wird. So können Sie verfolgen, wie lange ein Betrachter braucht, um ein bestimmtes Target zu sehen. Selbst wenn das Target später wieder in den Viewport gescrollt wird, liefert diese Eigenschaft die neue Zeit. Dies kann verwendet werden, um die Zeit für das Ein- und Ausblenden eines Targets aus dem Root-Element zu verfolgen.
Während all diese Informationen uns zur Verfügung stehen, wann immer eine Schnittstellenänderung beobachtet wird, werden sie uns auch zur Verfügung gestellt, wenn der Observer zum ersten Mal gestartet wird. Zum Beispiel rufen die Observer auf der Seite beim Laden der Seite sofort die Callback-Funktion auf und stellen den aktuellen Status jedes von ihnen beobachteten Target-Elements zur Verfügung.
Dies ist eine Fülle von Daten über die Beziehungen von Elementen auf der Seite, die auf sehr performante Weise bereitgestellt werden.
Intersection Observer Methoden
Intersection Observer hat drei bemerkenswerte Methoden: observe(), unobserve() und disconnect().
observe(): Dieobserve()-Methode nimmt eine DOM-Referenz auf ein Target-Element entgegen, das der Liste der vom Observer beobachteten Elemente hinzugefügt werden soll. Ein Observer kann mehr als ein Target-Element haben, aber diese Methode kann jeweils nur ein Target aufnehmen.unobserve(): Dieunobserve()-Methode nimmt eine DOM-Referenz auf ein Target-Element entgegen, das aus der Liste der vom Observer beobachteten Elemente entfernt werden soll.disconnect(): Diedisconnect()-Methode veranlasst den Observer, die Beobachtung aller seiner Target-Elemente einzustellen. Der Observer selbst ist noch aktiv, hat aber keine Targets. Nachdisconnect()können Target-Elemente immer noch mitobserve()an den Observer übergeben werden.
Diese Methoden ermöglichen das Beobachten und Aufheben der Beobachtung von Target-Elementen, aber es gibt keine Möglichkeit, die Optionen zu ändern, die dem Observer bei seiner Erstellung übergeben wurden. Sie müssen den Observer manuell neu erstellen, wenn andere Optionen erforderlich sind.
Performance: Intersection Observer vs. Scroll Events
Bei meiner Erkundung des Intersection Observer und wie er sich im Vergleich zur Verwendung von Scroll-Ereignissen verhält, wusste ich, dass ich einige Leistungstests durchführen musste. Ein völlig unwissenschaftlicher Aufwand wurde somit mit Puppeteer erstellt. Der Einfachheit halber wollte ich nur eine allgemeine Vorstellung von der Leistungsdifferenz zwischen den beiden bekommen. Daher wurden drei einfache Tests erstellt.
Zuerst habe ich eine grundlegende HTML-Datei erstellt, die einhundert Divs mit etwas Höhe enthielt, um eine lange Scroll-Seite zu erzeugen. Mit einem aktiven einfachen http-Server lud ich die HTML-Datei mit Puppeteer, startete eine Trace, zwang die Seite, in voreingestellten Intervallen nach unten zu scrollen, bis zum Ende, beendete den Trace, sobald das Ende erreicht war, und speicherte schließlich die Ergebnisse des Trace. Ich habe auch dafür gesorgt, dass der Test mehrmals wiederholt und die Daten jedes Mal ausgegeben werden. Dann habe ich das Basis-HTML dupliziert und meinen JavaScript-Code in einem Skript-Tag für jede Art von Test, den ich ausführen wollte, geschrieben. Jeder Test hat zwei Dateien: eine für den Intersection Observer und die andere für Scroll-Ereignisse.
Der Zweck aller Tests ist es, zu erkennen, wann ein Target-Element bei 25%-Schritten nach oben durch den Viewport scrollt. Bei jedem Schritt wird eine CSS-Klasse angewendet, die die Hintergrundfarbe des Elements ändert. Mit anderen Worten, jedes Element hat DOM-Änderungen erfahren, die zu Repaints führen würden. Jeder Test wurde fünfmal auf zwei verschiedenen Maschinen ausgeführt: meinem Entwicklungs-Mac mit ziemlich aktueller Hardware und meiner persönlichen Windows 7-Maschine, die heutzutage wahrscheinlich durchschnittlich ist. Die Ergebnisse der Trace-Zusammenfassung von Scripting, Rendering, Painting und System wurden aufgezeichnet und dann gemittelt. Auch hier nichts allzu Wissenschaftliches – nur eine allgemeine Idee.
Der erste Test hat einen Observer oder ein Scroll-Ereignis mit jeweils einem Callback. Dies ist eine ziemlich Standardkonfiguration sowohl für den Observer als auch für das Scroll-Ereignis. Obwohl in diesem Fall das Scroll-Ereignis etwas mehr Arbeit leisten muss, da es versucht, die Daten zu simulieren, die der Observer standardmäßig bereitstellt. Sobald all diese Berechnungen durchgeführt sind, werden die Daten wie beim Observer in einem Entry-Array gespeichert. Dann ist die Funktionalität zum Entfernen und Anwenden von Klassen zwischen den beiden genau gleich. Ich drossele das Scroll-Ereignis ein wenig mit requestAnimationFrame.
Der zweite Test hat 100 Observer oder 100 Scroll-Ereignisse mit einem Callback für jeden Typ. Jedem Element wird sein eigener Observer und Event zugewiesen, aber die Callback-Funktion ist dieselbe. Das ist eigentlich ineffizient, da jeder Observer und jedes Event genau gleich funktioniert, aber ich wollte einen einfachen Stresstest, ohne 100 einzigartige Observer und Events erstellen zu müssen – obwohl ich viele Beispiele für die Verwendung des Observers auf diese Weise gesehen habe.
Der dritte Test hat 100 Observer oder 100 Scroll-Ereignisse mit 100 Callbacks für jeden Typ. Das bedeutet, jedes Element hat seinen eigenen Observer, Event und seine eigene Callback-Funktion. Dies ist natürlich furchtbar ineffizient, da dies alles duplizierte Funktionalität ist, die in riesigen Arrays gespeichert ist. Aber diese Ineffizienz ist der Punkt dieses Tests.

In den obigen Diagrammen repräsentiert die erste Spalte unsere Baseline, bei der überhaupt kein JavaScript ausgeführt wurde. Die nächsten beiden Spalten repräsentieren den ersten Testtyp. Der Mac lief beide ziemlich gut, wie ich es von einem High-End-Entwicklungsgerät erwarten würde. Die Windows-Maschine erzählte eine andere Geschichte. Für mich ist der Hauptinteressenspunkt die rote Scripting-Ausgabe. Auf dem Mac betrug der Unterschied etwa 88 ms für den Observer, während es für das Scroll-Ereignis etwa 300 ms waren. Das Gesamtergebnis auf dem Mac ist für beide ziemlich ähnlich, aber das Scripting wurde mit dem Scroll-Ereignis stark beeinträchtigt. Für die Windows-Maschine ist es weitaus, weitaus schlimmer. Der Observer lag bei etwa 150 ms gegenüber etwa 1400 ms für den ersten und einfachsten Test der drei.
Für den zweiten Test beginnen wir, die Ineffizienz des Scroll-Tests deutlicher zu erkennen. Sowohl der Mac als auch die Windows-Maschine führten den Observer-Test mit weitgehend den gleichen Ergebnissen wie zuvor aus. Für den Scroll-Ereignis-Test wird das Scripting stärker beansprucht, um die gegebenen Aufgaben zu erledigen. Der Mac sprang auf fast eine Sekunde Scripting, während die Windows-Maschine auf schwindelerregende 3200 ms sprang.
Für den dritten Test wurde es glücklicherweise nicht schlimmer. Die Ergebnisse sind ungefähr die gleichen wie beim zweiten Test. Eine Sache, die man beachten sollte, ist, dass die Ergebnisse für den Observer bei allen drei Tests für beide Computer konsistent waren. Trotz keinerlei Effizienzbemühungen für die Observer-Tests übertraf der Intersection Observer die Scroll-Ereignisse um eine deutliche Marge.
Nach meinen nicht-wissenschaftlichen Tests auf meinen beiden Maschinen hatte ich also eine gute Vorstellung von den Leistungsunterschieden zwischen Scroll-Ereignissen und dem Intersection Observer. Ich bin sicher, dass ich mit etwas Aufwand die Scroll-Ereignisse effizienter gestalten könnte, aber lohnt sich das? Es gibt Fälle, in denen die Präzision der Scroll-Ereignisse notwendig ist, aber in den meisten Fällen reicht der Intersection Observer gut aus – besonders, da er anscheinend ohne Aufwand weitaus effizienter ist.
Verständnis der intersectionRatio-Eigenschaft
Die Eigenschaft intersectionRatio, die uns von IntersectionObserverEntry zur Verfügung gestellt wird, repräsentiert den Prozentsatz des Target-Elements, der sich bei einer Schnittstellenänderung innerhalb der Grenzen des Root-Elements befindet. Ich fand heraus, dass ich zunächst nicht ganz verstand, was dieser Wert tatsächlich repräsentierte. Aus irgendeinem Grund dachte ich, es sei eine einfache Null-bis-Hundert-Prozent-Darstellung des Erscheinungsbildes des Target-Elements, was es irgendwie auch ist. Es ist an die Schwellenwerte gebunden, die dem Observer bei seiner Erstellung übergeben wurden. Es könnte beispielsweise verwendet werden, um zu bestimmen, welcher Schwellenwert die Ursache für die gerade ausgelöste Schnittstellenänderung war. Die von ihm gelieferten Werte sind jedoch nicht immer eindeutig.
Nehmen Sie dieses Demo zum Beispiel
Siehe den Pen
Intersection Observer: intersectionRatio von Travis Almand (@talmand)
auf CodePen.
In diesem Demo wurde dem Observer das Elternelement als Root-Element zugewiesen. Dem untergeordneten Element mit dem Zielfarbhintergrund wurde die Rolle des Target-Elements zugewiesen. Das Schwellenwert-Array wurde mit 100 Einträgen erstellt, mit der Sequenz 0, 0,01, 0,02, 0,03 und so weiter bis 1. Der Observer löst bei jedem Prozentsatz des erscheinenden oder verschwindenden Target-Elements innerhalb des Root-Elements aus, sodass bei jeder Änderung des Verhältnisses um mindestens ein Prozent die unter der Box angezeigten Ausgabetexte aktualisiert werden. Falls Sie neugierig sind, wurde dieser Schwellenwert mit diesem Code erreicht
[...Array(100).keys()].map(x => x / 100) }
Ich empfehle nicht, Ihre Schwellenwerte auf diese Weise für die typische Verwendung in Projekten festzulegen.
Zuerst ist das Target-Element vollständig im Root-Element enthalten und die Ausgabe über den Buttons zeigt ein Verhältnis von eins. Es sollte beim ersten Laden eins sein, aber wir werden bald sehen, dass das Verhältnis nicht immer präzise ist; es ist möglich, dass die Zahl irgendwo zwischen 0,99 und 1 liegt. Das erscheint zwar seltsam, kann aber vorkommen. Behalten Sie das im Hinterkopf, wenn Sie Prüfungen gegen das Verhältnis gleich einem bestimmten Wert erstellen.
Wenn Sie auf die Schaltfläche „Links“ klicken, wird das Target-Element nach links verschoben, sodass die Hälfte davon im Root-Element und die andere Hälfte außerhalb liegt. Das intersectionRatio sollte sich dann auf 0,5 oder etwas Ähnliches ändern. Wir wissen jetzt, dass die Hälfte des Target-Elements mit dem Root-Element kollidiert, aber wir haben keine Ahnung, wo es sich befindet. Mehr dazu später.
Das Klicken auf die Schaltfläche „Oben“ macht im Grunde dasselbe. Es verschiebt das Target-Element an den oberen Rand des Root-Elements, wobei wieder die Hälfte davon darin und die andere Hälfte außerhalb liegt. Und wieder sollte das intersectionRatio irgendwo um 0,5 liegen. Obwohl sich das Target-Element an einem völlig anderen Ort befindet als zuvor, ist das resultierende Verhältnis dasselbe.
Wenn Sie auf die Schaltfläche „Ecke“ klicken, wird das Target-Element erneut in die obere rechte Ecke des Root-Elements verschoben. Zu diesem Zeitpunkt ist nur ein Viertel des Target-Elements innerhalb des Root-Elements. Das intersectionRatio sollte dies mit einem Wert von etwa 0,25 widerspiegeln. Wenn Sie auf „Mitte“ klicken, wird das Target-Element wieder in die Mitte verschoben und vollständig innerhalb des Root-Elements platziert.
Wenn wir auf die Schaltfläche „Groß“ klicken, wird die Höhe des Target-Elements so geändert, dass es höher als das Root-Element ist. Das intersectionRatio sollte irgendwo um 0,8 liegen, mit ein paar Zehntausendstel Prozent Abweichung. Das ist der knifflige Teil, wenn man sich auf intersectionRatio verlässt. Das Erstellen von Code basierend auf den an den Observer übergebenen Schwellenwerten macht es möglich, Schwellenwerte zu haben, die niemals ausgelöst werden. In diesem „großen“ Beispiel wird jeder Code, der auf einem Schwellenwert von 1 basiert, fehlschlagen. Berücksichtigen Sie auch Situationen, in denen das Root-Element vergrößert werden kann, z. B. wenn der Viewport von Hoch- zu Querformat gedreht wird.
Die Position finden
Wie wissen wir also, wo sich das Target-Element relativ zum Root-Element befindet? Glücklicherweise werden die Daten für diese Berechnung von IntersectionObserverEntry bereitgestellt, sodass wir nur einfache Vergleiche durchführen müssen.
Betrachten Sie dieses Demo
Siehe den Pen
Intersection Observer: Finding the Position von Travis Almand (@talmand)
auf CodePen.
Die Einrichtung für dieses Demo ist weitgehend dieselbe wie bei der vorherigen. Der Elterncontainer ist das Root-Element und das Kind darin mit dem Zielfarbhintergrund ist das Target-Element. Der Schwellenwert ist ein Array aus 0, 0,5 und 1. Während Sie innerhalb des Root-Elements scrollen, erscheint das Target und seine Position wird in der Ausgabe über den Schaltflächen gemeldet.
Hier ist der Code, der diese Prüfungen durchführt
const output = document.querySelector('#output pre');
function io_callback (entries) {
const ratio = entries[0].intersectionRatio;
const boundingRect = entries[0].boundingClientRect;
const intersectionRect = entries[0].intersectionRect;
if (ratio === 0) {
output.innerText = 'outside';
} else if (ratio < 1) {
if (boundingRect.top < intersectionRect.top) {
output.innerText = 'on the top';
} else {
output.innerText = 'on the bottom';
}
} else {
output.innerText = 'inside';
}
}
Ich möchte darauf hinweisen, dass ich nicht über das Array der Einträge loope, da ich weiß, dass es immer nur einen Eintrag geben wird, da es nur ein Target gibt. Ich nehme eine Abkürzung, indem ich entries[0] verwende.
Sie werden sehen, dass ein Verhältnis von Null das Target auf „außen“ platziert. Ein Verhältnis von weniger als eins platziert es entweder oben oder unten. Das lässt uns erkennen, ob die „Oberseite“ des Targets kleiner ist als die Oberseite von intersectionRect, was tatsächlich bedeutet, dass es weiter oben auf der Seite liegt und als „oben“ betrachtet wird. Tatsächlich würde die Überprüfung gegen die „Oberseite“ des Root-Elements auch hier funktionieren. Logischerweise, wenn das Target nicht oben ist, muss es unten sein. Wenn das Verhältnis eins ergibt, ist es „innerhalb“ des Root-Elements. Die Überprüfung der horizontalen Position erfolgt auf die gleiche Weise, nur dass sie mit den Eigenschaften links oder rechts durchgeführt wird.
Dies ist Teil der Effizienz der Verwendung des Intersection Observer. Entwickler müssen diese Informationen nicht von verschiedenen Stellen aus bei einem gedrosselten Scroll-Ereignis (das ohnehin recht häufig ausgelöst wird) abrufen und dann die zugehörigen Berechnungen durchführen, um all das herauszufinden. Es wird vom Observer bereitgestellt und alles, was benötigt wird, ist eine einfache if-Prüfung.
Zuerst ist das Target-Element höher als das Root-Element, daher wird es nie als „innen“ gemeldet. Klicken Sie auf die Schaltfläche „Target-Größe umschalten“, um es kleiner als das Root-Element zu machen. Jetzt kann das Target-Element beim Scrollen nach oben und unten innerhalb des Root-Elements liegen.
Stellen Sie die ursprüngliche Größe des Target-Elements wieder her, indem Sie erneut auf „Target-Größe umschalten“ klicken, und klicken Sie dann auf die Schaltfläche „Root-Größe umschalten“. Dies vergrößert das Root-Element so, dass es höher als das Target-Element ist. Auch hier ist es beim Scrollen nach oben und unten möglich, dass das Target-Element „innerhalb“ des Root-Elements liegt.
Dieses Demo demonstriert zwei Dinge über den Intersection Observer: wie die Position des Target-Elements relativ zum Root-Element bestimmt wird und was passiert, wenn die beiden Elemente in der Größe geändert werden. Diese Reaktion auf Größenänderungen ist ein weiterer Vorteil gegenüber Scroll-Ereignissen – kein Code, der an ein Größenänderungsereignis angepasst werden muss.
Erstellung eines „Position Sticky“-Ereignisses
Der „sticky“-Wert für die CSS-Eigenschaft position kann ein nützliches Feature sein, ist aber in Bezug auf CSS und JavaScript etwas einschränkend. Das Styling des Sticky-Elements kann nur ein Design haben, sei es im normalen Zustand oder im Sticky-Zustand. Es gibt keine einfache Möglichkeit, den Zustand für JavaScript zu kennen, um auf diese Änderungen zu reagieren. Bisher gibt es keine Pseudoklasse oder kein JavaScript-Ereignis, das uns auf die sich ändernde Zustandsänderung des Elements aufmerksam macht.
Ich habe Beispiele gesehen, wie man eine Art Ereignis für das Sticky-Positioning mit sowohl Scroll-Ereignissen als auch dem Intersection Observer verwendet. Die Lösungen mit Scroll-Ereignissen haben immer Probleme, die denen der Verwendung von Scroll-Ereignissen für andere Zwecke ähneln. Die übliche Lösung mit einem Observer verwendet ein „Dummy“-Element, das wenig anderen Zweck hat, als ein Target für den Observer zu sein. Ich vermeide es, Elemente mit nur einem Zweck zu verwenden, also habe ich beschlossen, mit dieser speziellen Idee zu experimentieren.
In diesem Demo scrollen Sie nach oben und unten, um zu sehen, wie die Abschnittstitel auf das „Sticky“-Sein in ihren jeweiligen Abschnitten reagieren.
Siehe den Pen
Intersection Observer: Position Sticky Event von Travis Almand (@talmand)
auf CodePen.
Dies ist ein Beispiel dafür, wie erkannt wird, wann sich ein Sticky-Element am oberen Rand des Scrolling-Containers befindet, sodass eine Klasse auf das Element angewendet werden kann. Dies wird durch die Nutzung einer interessanten Eigenart des DOM erreicht, wenn dem Observer ein bestimmter rootMargin zugewiesen wird. Die angegebenen Werte sind
rootMargin: '0px 0px -100% 0px'
Dies drückt den unteren Rand der Begrenzung des Root-Elements an die Oberseite des Root-Elements, was einen schmalen Bereich für die Erkennung von Schnittpunkten hinterlässt, der null Pixel beträgt. Ein Zielelement, das diesen Null-Pixel-Bereich berührt, löst die Schnittpunktänderung aus, obwohl es sozusagen rechnerisch nicht existiert. Betrachten Sie, dass wir Elemente im DOM haben können, die mit einer kollabierten Höhe von null existieren.
Diese Lösung nutzt dies aus, indem sie erkennt, dass das "sticky" Element immer in seiner "sticky" Position am oberen Rand des Root-Elements ist. Wenn das Scrollen fortgesetzt wird, bewegt sich das "sticky" Element schließlich aus dem Blickfeld und die Schnittpunktprüfung stoppt. Daher fügen wir die Klasse basierend auf der Eigenschaft isIntersecting des Entry-Objekts hinzu und entfernen sie wieder.
Hier ist das HTML
<section>
<div class="sticky-container">
<div class="sticky-content">
<span>§</span>
<h2>Section 1</h2>
</div>
</div>
{{ content here }}
</section>
Die äußere Div mit der Klasse sticky-container ist das Ziel für unseren Observer. Diese Div wird als "sticky" Element gesetzt und fungiert als Container. Das Element, das verwendet wird, um das Element basierend auf dem "sticky" Zustand zu stylen und zu ändern, ist die sticky-content Div und ihre Kindelemente. Dies stellt sicher, dass das eigentliche "sticky" Element immer in Kontakt mit dem geschrumpften rootMargin am oberen Rand des Root-Elements steht.
Hier ist das CSS
.sticky-content {
position: relative;
transition: 0.25s;
}
.sticky-content span {
display: inline-block;
font-size: 20px;
opacity: 0;
overflow: hidden;
transition: 0.25s;
width: 0;
}
.sticky-content h2 {
display: inline-block;
}
.sticky-container {
position: sticky;
top: 0;
}
.sticky-container.active .sticky-content {
background-color: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 10px;
}
.sticky-container.active .sticky-content span {
opacity: 1;
transition: 0.25s 0.5s;
width: 20px;
}
Sie werden sehen, dass .sticky-container unser "sticky" Element am oberen Rand von Null erstellt. Der Rest ist eine Mischung aus Stilen für den regulären Zustand in .sticky-content und dem "sticky" Zustand mit .active .sticky-content. Auch hier können Sie praktisch alles mit dem "sticky" Content-Div machen, was Sie wollen. In dieser Demo gibt es ein verstecktes Abschnittssymbol, das bei einer verzögerten Transition erscheint, wenn der "sticky" Zustand aktiv ist. Dieser Effekt wäre ohne etwas, das hilft, wie den Intersection Observer, schwierig.
Das JavaScript
const stickyContainers = document.querySelectorAll('.sticky-container');
const io_options = {
root: document.body,
rootMargin: '0px 0px -100% 0px',
threshold: 0
};
const io_observer = new IntersectionObserver(io_callback, io_options);
stickyContainers.forEach(element => {
io_observer.observe(element);
});
function io_callback (entries, observer) {
entries.forEach(entry => {
entry.target.classList.toggle('active', entry.isIntersecting);
});
}
Dies ist tatsächlich ein sehr unkompliziertes Beispiel für die Verwendung des Intersection Observers für diese Aufgabe. Das Einzige, was ungewöhnlich ist, ist der Wert -100% in rootMargin. Beachten Sie, dass dies auch für die anderen drei Seiten wiederholt werden kann; es erfordert lediglich einen neuen Observer mit seinem eigenen eindeutigen rootMargin mit -100% für die entsprechende Seite. Es wird mehrere eindeutige "sticky" Container mit ihren eigenen Klassen geben, wie z.B. sticky-container-top und sticky-container-bottom.
Die Einschränkung dabei ist, dass die Top-, Right-, Bottom- oder Left-Eigenschaft für das "sticky" Element immer Null sein muss. Technisch gesehen könnten Sie einen anderen Wert verwenden, aber dann müssten Sie die Mathematik durchführen, um den richtigen Wert für rootMargin zu ermitteln. Dies ist leicht zu machen, aber wenn sich Dinge vergrößern oder verkleinern, muss nicht nur die Mathematik erneut durchgeführt werden, sondern der Observer muss gestoppt und mit dem neuen Wert neu gestartet werden. Es ist einfacher, die Positionseigenschaft auf Null zu setzen und die inneren Elemente zu verwenden, um Dinge so zu gestalten, wie Sie es möchten.
Kombination mit Scroll-Ereignissen
Wie wir in einigen der bisherigen Demos gesehen haben, kann das intersectionRatio ungenau und etwas einschränkend sein. Die Verwendung von Scroll-Ereignissen kann präziser sein, aber auf Kosten der Leistungseffizienz. Was wäre, wenn wir beides kombinieren würden?
Siehe den Stift
Intersection Observer: Scroll Events von Travis Almand (@talmand)
auf CodePen.
In dieser Demo haben wir einen Intersection Observer erstellt und die Callback-Funktion dient dem alleinigen Zweck, einen Event-Listener für das Scroll-Ereignis auf dem Root-Element hinzuzufügen und zu entfernen. Wenn das Ziel das Root-Element zuerst betritt, wird der Scroll-Event-Listener erstellt und dann entfernt, wenn das Ziel das Root-Element verlässt. Während des Scrollens zeigt die Ausgabe einfach den Zeitstempel jedes Ereignisses an, um zu zeigen, wie er sich in Echtzeit ändert – viel präziser als der Observer allein.
Das Setup für HTML und CSS ist zu diesem Zeitpunkt ziemlich Standard, daher hier das JavaScript.
const root = document.querySelector('#root');
const target = document.querySelector('#target');
const output = document.querySelector('#output pre');
const io_options = {
root: root,
rootMargin: '0px',
threshold: 0
};
let io_observer;
function scrollingEvents (e) {
output.innerText = e.timeStamp;
}
function io_callback (entries) {
if (entries[0].isIntersecting) {
root.addEventListener('scroll', scrollingEvents);
} else {
root.removeEventListener('scroll', scrollingEvents);
output.innerText = 0;
}
}
io_observer = new IntersectionObserver(io_callback, io_options);
io_observer.observe(target);
Dies ist ein ziemlich Standardbeispiel. Beachten Sie, dass wir den Schwellenwert auf Null setzen wollen, da wir sonst mehrere Event-Listener gleichzeitig erhalten, wenn es mehr als einen Schwellenwert gibt. Die Callback-Funktion ist das, was uns interessiert, und selbst das ist ein einfaches Setup: Hinzufügen und Entfernen des Event-Listeners in einem if-else-Block. Die Callback-Funktion des Ereignisses aktualisiert einfach die Div in der Ausgabe. Immer wenn das Ziel eine Schnittpunktänderung auslöst und nicht mit dem Root übereinstimmt, setzen wir die Ausgabe auf Null zurück.
Dies bietet die Vorteile von Intersection Observer und Scroll-Ereignissen. Stellen Sie sich eine Scroll-Animationsbibliothek vor, die nur dann funktioniert, wenn der Abschnitt der Seite, der sie benötigt, tatsächlich sichtbar ist. Die Bibliothek und die Scroll-Ereignisse sind nicht ineffizient auf der gesamten Seite aktiv.
Interessante Unterschiede bei Browsern
Sie fragen sich wahrscheinlich, wie viel Browserunterstützung es für den Intersection Observer gibt. Ziemlich viel, tatsächlich!
Diese Daten zur Browserunterstützung stammen von Caniuse, wo es weitere Details gibt. Eine Zahl bedeutet, dass der Browser die Funktion ab dieser Version unterstützt.
Desktop
| Chrome | Firefox | IE | Edge | Safari |
|---|---|---|---|---|
| 58 | 55 | Nein | 16 | 12.1 |
Mobil / Tablet
| Android Chrome | Android Firefox | Android | iOS Safari |
|---|---|---|---|
| 127 | 127 | 127 | 12.2-12.5 |
Alle wichtigen Browser unterstützen ihn schon seit einiger Zeit. Wie Sie vielleicht erwarten, unterstützt der Internet Explorer ihn auf keiner Ebene, aber es gibt ein Polyfill von der W3C, das sich darum kümmert.
Als ich verschiedene Ideen mit dem Intersection Observer experimentierte, stieß ich auf ein paar Beispiele, die sich zwischen Firefox und Chrome unterschiedlich verhalten. Ich würde diese Beispiele nicht auf einer Produktionsseite verwenden, aber die Verhaltensweisen sind interessant.
Hier ist das erste Beispiel
Siehe den Stift
Intersection Observer: Animated Transform von Travis Almand (@talmand)
auf CodePen.
Das Zielelement bewegt sich innerhalb des Root-Elements mithilfe der CSS-Eigenschaft transform. Die Demo hat eine CSS-Animation, die das Zielelement auf der horizontalen Achse in und aus dem Root-Element transformiert. Wenn das Zielelement in das Root-Element eintritt oder es verlässt, wird das intersectionRatio aktualisiert.
Wenn Sie diese Demo in Firefox ansehen, sollten Sie sehen, wie sich das intersectionRatio ordnungsgemäß aktualisiert, während sich das Zielelement hin und her schiebt. Chrome verhält sich anders. Das Standardverhalten dort aktualisiert die Anzeige von intersectionRatio überhaupt nicht. Es scheint, dass Chrome kein Zielelement im Auge behält, das mit CSS transformiert wird. Wenn wir jedoch die Maus bewegen, während sich das Zielelement in und aus dem Root-Element bewegt, aktualisiert sich die Anzeige von intersectionRatio tatsächlich. Ich vermute, dass Chrome den Observer nur dann "aktiviert", wenn eine Form von Benutzerinteraktion stattfindet.
Hier ist das zweite Beispiel
Siehe den Stift
Intersection Observer: Animated Clip-Path von Travis Almand (@talmand)
auf CodePen.
Diesmal animieren wir einen clip-path, der ein Quadrat in einen Kreis in einer wiederholten Schleife verwandelt. Das Quadrat hat die gleiche Größe wie das Root-Element, daher wird das erhaltene intersectionRatio immer kleiner als eins sein. Während der clip-path animiert, aktualisiert Firefox die Anzeige von intersectionRatio überhaupt nicht. Und diesmal funktioniert das Bewegen der Maus nicht. Firefox ignoriert einfach die sich ändernde Größe des Elements. Chrome hingegen aktualisiert tatsächlich die Anzeige von intersectionRatio in Echtzeit. Dies geschieht auch ohne Benutzerinteraktion.
Dies scheint aufgrund eines Teils der Spezifikation zu geschehen, der besagt, dass die Grenzen des Schnittbereichs (die intersectionRect) das Zuschneiden des Zielelements einschließen sollen.
Wenn container eine Überlaufbeschneidung oder eine CSS-Eigenschaft clip-path hat, aktualisieren Sie intersectionRect, indem Sie den Ausschnitt des Containers anwenden.
Wenn also ein Ziel beschnitten wird, werden die Grenzen des Schnittbereichs neu berechnet. Firefox hat dies offenbar noch nicht implementiert.
Intersection Observer, Version 2
Was hält also die Zukunft für diese API bereit?
Es gibt Vorschläge von Google, die dem Observer ein interessantes Feature hinzufügen werden. Obwohl der Intersection Observer uns sagt, wann ein Zielelement die Grenzen eines Root-Elements überschreitet, bedeutet das nicht unbedingt, dass dieses Element für den Benutzer tatsächlich sichtbar ist. Es könnte eine Opazität von Null haben oder von einem anderen Element auf der Seite verdeckt sein. Was wäre, wenn der Observer verwendet werden könnte, um diese Dinge zu bestimmen?
Bitte beachten Sie, dass wir uns bei einem solchen Feature noch in den Anfängen befinden und es nicht in Produktionscode verwendet werden sollte. Hier ist der aktualisierte Vorschlag mit den Unterschieden zur ersten Version der Spezifikation hervorgehoben.
Wenn Sie die Demos in diesem Artikel mit Chrome angesehen haben, haben Sie möglicherweise einige Dinge in der Konsole bemerkt – wie z.B. entries-Objekteigenschaften, die in Firefox nicht erscheinen. Hier ist ein Beispiel dafür, was Firefox in der Konsole protokolliert
IntersectionObserver
root: null
rootMargin: "0px 0px 0px 0px"
thresholds: Array [ 0 ]
<prototype>: IntersectionObserverPrototype { }
IntersectionObserverEntry
boundingClientRect: DOMRect { x: 9, y: 779, width: 707, ... }
intersectionRatio: 0
intersectionRect: DOMRect { x: 0, y: 0, width: 0, ... }
isIntersecting: false
rootBounds: null
target: <div class="item">
time: 261
<prototype>: IntersectionObserverEntryPrototype { }
Hier ist nun die gleiche Ausgabe des gleichen Konsolen-Codes in Chrome
IntersectionObserver
delay: 500
root: null
rootMargin: "0px 0px 0px 0px"
thresholds: [0]
trackVisibility: true
__proto__: IntersectionObserver
IntersectionObserverEntry
boundingClientRect: DOMRectReadOnly {x: 9, y: 740, width: 914, height: 146, top: 740, ...}
intersectionRatio: 0
intersectionRect: DOMRectReadOnly {x: 0, y: 0, width: 0, height: 0, top: 0, ...}
isIntersecting: false
isVisible: false
rootBounds: null
target: div.item
time: 355.6550000066636
__proto__: IntersectionObserverEntry
Es gibt einige Unterschiede in der Darstellung einiger Eigenschaften, wie z.B. target und prototype, aber sie funktionieren in beiden Browsern gleich. Was anders ist, ist, dass Chrome ein paar zusätzliche Eigenschaften hat, die in Firefox nicht erscheinen. Das observer-Objekt hat ein boolesches Feld namens trackVisibility, eine Zahl namens delay und das entry-Objekt hat ein boolesches Feld namens isVisible. Dies sind die neu vorgeschlagenen Eigenschaften, die versuchen zu bestimmen, ob das Zielelement für den Benutzer tatsächlich sichtbar ist.
Ich werde eine kurze Erklärung dieser Eigenschaften geben, aber bitte lesen Sie diesen Artikel, wenn Sie mehr Details wünschen.
Die Eigenschaft trackVisibility ist das boolesche Feld, das dem Observer im options-Objekt übergeben wird. Dies weist den Browser an, die teurere Aufgabe der Bestimmung der tatsächlichen Sichtbarkeit des Zielelements zu übernehmen.
Die Eigenschaft delay ist das, was Sie vielleicht erraten: Sie verzögert den Callback der Schnittpunktänderung um die angegebene Zeit in Millisekunden. Dies ist im Grunde so, als ob der Code Ihrer Callback-Funktion in setTimeout verpackt wäre. Damit trackVisibility funktioniert, ist dieser Wert erforderlich und muss mindestens 100 betragen. Wenn kein korrekter Wert angegeben wird, zeigt die Konsole diesen Fehler an und der Observer wird nicht erstellt.
Uncaught DOMException: Failed to construct 'IntersectionObserver': To enable the
'trackVisibility' option, you must also use a 'delay' option with a value of at
least 100. Visibility is more expensive to compute than the basic intersection;
enabling this option may negatively affect your page's performance.
Please make sure you really need visibility tracking before enabling the
'trackVisibility' option.
Die Eigenschaft isVisible im entry-Objekt des Ziels ist das boolesche Feld, das das Ergebnis der Sichtbarkeitsverfolgung meldet. Dies kann wie isIntersecting als Teil jedes Codes verwendet werden.
Bei all meinen Experimenten mit diesen Funktionen scheint es eher zufällig zu funktionieren. Zum Beispiel funktioniert delay konsistent, aber isVisible meldet nicht immer "true" – zumindest bei mir – wenn das Element deutlich sichtbar ist. Manchmal ist dies beabsichtigt, da die Spezifikation auch False-Negatives zulässt. Das könnte die inkonsistenten Ergebnisse erklären.
Ich persönlich kann es kaum erwarten, dass diese Funktion vollständiger und in allen Browsern, die Intersection Observer unterstützen, funktioniert.
Jetzt ist eure Wache vorüber
So endet meine Forschung über den Intersection Observer, eine API, die ich auf jeden Fall in zukünftigen Projekten nutzen möchte. Ich habe viele Nächte damit verbracht, zu recherchieren, zu experimentieren und Beispiele zu erstellen, um zu verstehen, wie er funktioniert. Aber das Ergebnis war dieser Artikel, plus einige neue Ideen, wie man verschiedene Funktionen des Observers nutzen kann. Außerdem habe ich das Gefühl, dass ich zu diesem Zeitpunkt effektiv erklären kann, wie der Observer funktioniert, wenn ich gefragt werde. Hoffentlich hilft Ihnen dieser Artikel dabei, dasselbe zu tun.
Von Lee Rowlands
Das ist großartig!!!
~~Eine Erklärung~~ Eine Meisterklasse darin, wie der Intersection Observer beobachtet
FTFY