Teraz mnie widzisz: jak odroczyć, leniwie ładować i działać za pomocą IntersectionObserver
Opublikowany: 2022-03-10Dawno, dawno temu żył twórca stron internetowych, który skutecznie przekonał swoich klientów, że witryny nie powinny wyglądać tak samo we wszystkich przeglądarkach, dbał o dostępność i był jednym z pierwszych użytkowników siatek CSS. Ale w głębi serca to performance był jego prawdziwą pasją: stale optymalizował, minimalizował, monitorował, a nawet stosował psychologiczne triki w swoich projektach.
Pewnego dnia dowiedział się o leniwym wczytywaniu się obrazów i innych zasobów, które nie są od razu widoczne dla użytkowników i nie są niezbędne do renderowania treści na ekranie. To był początek świtu: deweloper wkroczył w zły świat leniwie ładujących się wtyczek jQuery (a może nie tak zły świat atrybutów async
i defer
). Niektórzy twierdzą nawet, że trafił prosto w sedno całego zła: świat słuchaczy zdarzeń scroll
. Nigdy nie dowiemy się na pewno, gdzie trafił, ale z drugiej strony ten programista jest absolutnie fikcyjny, a jakiekolwiek podobieństwo do jakiegokolwiek programisty jest po prostu przypadkowe.
Cóż, teraz można powiedzieć, że puszka Pandory została otwarta i że nasz fikcyjny programista nie umniejsza tego problemu. W dzisiejszych czasach priorytetyzacja treści w części strony widocznej na ekranie stała się niezwykle ważna dla wydajności naszych projektów internetowych, zarówno z punktu widzenia szybkości, jak i wagi strony.
W tym artykule wyjdziemy z ciemności scroll
i porozmawiamy o nowoczesnym sposobie leniwego ładowania zasobów. Nie tylko leniwe ładowanie obrazów, ale także wczytywanie dowolnego zasobu. Co więcej, technika, o której będziemy dzisiaj mówić, jest w stanie znacznie więcej niż tylko leniwe ładowanie zasobów: będziemy w stanie zapewnić dowolny rodzaj odroczonej funkcjonalności w oparciu o widoczność elementów dla użytkowników.
Panie i panowie, porozmawiajmy o API Intersection Observer. Ale zanim zaczniemy, spójrzmy na krajobraz nowoczesnych narzędzi, który doprowadził nas do IntersectionObserver
.
Rok 2017 był bardzo dobrym rokiem dla narzędzi wbudowanych w nasze przeglądarki, które pomogły nam bez większego wysiłku poprawić jakość i styl naszego kodu. W dzisiejszych czasach wydaje się, że sieć odchodzi od sporadycznych rozwiązań opartych na bardzo różnych rozwiązaniach, niż rozwiązywanie bardzo typowe dla bardziej dobrze zdefiniowanego podejścia do interfejsów Observera (lub po prostu „Observers”): Dobrze obsługiwany MutationObserver zyskał nowych członków rodziny, którzy zostali szybko przyjęte w nowoczesnych przeglądarkach:
- Obserwator skrzyżowania i
- PerformanceObserver (jako część specyfikacji Performance Timeline Level 2).
Jeszcze jeden potencjalny członek rodziny, FetchObserver, jest w toku i prowadzi nas bardziej w krainy proxy sieci, ale dzisiaj chciałbym porozmawiać o front-endzie.
PerformanceObserver
i IntersectionObserver
mają na celu pomóc programistom front-end poprawić wydajność ich projektów w różnych punktach. Pierwsza z nich daje nam narzędzie do Realnego Monitorowania Użytkownika, druga zaś jest narzędziem, które zapewnia nam wymierną poprawę wydajności. Jak wspomniano wcześniej, w tym artykule przyjrzymy się dokładnie temu drugiemu: IntersectionObserver . Aby w szczególności zrozumieć mechanikę IntersectionObserver
, powinniśmy przyjrzeć się, jak powinien działać ogólny Observer we współczesnej sieci.
Wskazówka dla profesjonalistów: możesz pominąć teorię i od razu zagłębić się w mechanikę IntersectionObserver lub, jeszcze dalej, bezpośrednio do możliwych zastosowań IntersectionObserver
.
Obserwator kontra zdarzenie
„Obserwator”, jak sama nazwa wskazuje, ma za zadanie obserwować coś, co dzieje się w kontekście strony. Obserwatorzy mogą obserwować, co dzieje się na stronie, na przykład zmiany DOM. Mogą również obserwować zdarzenia cyklu życia strony. Obserwatorzy mogą również uruchomić niektóre funkcje zwrotne. Teraz uważny czytelnik może od razu zauważyć problem i zapytać: „Więc o co chodzi? Czy nie mamy już wydarzeń w tym celu? Co wyróżnia Obserwatorów?” Bardzo dobry punkt! Przyjrzyjmy się bliżej i rozwiążmy to.
Kluczowa różnica między zwykłym Zdarzeniem a Obserwatorem polega na tym, że domyślnie ten pierwszy reaguje synchronicznie na każde wystąpienie Zdarzenia, wpływając na reakcję głównego wątku, podczas gdy ten drugi powinien reagować asynchronicznie, nie wpływając tak bardzo na wydajność. Przynajmniej dotyczy to obecnie prezentowanych Obserwatorów: wszyscy zachowują się asynchronicznie i nie sądzę, aby miało to się zmienić w przyszłości.
Prowadzi to do głównej różnicy w obsłudze wywołań zwrotnych obserwatorów, która może zmylić początkujących: asynchroniczna natura obserwatorów może skutkować przekazaniem kilku obserwowalnych do funkcji wywołania zwrotnego w tym samym czasie. Z tego powodu funkcja wywołania zwrotnego powinna oczekiwać nie pojedynczego wpisu, ale Array
wpisów (nawet jeśli czasami tablica zawiera tylko jeden wpis).
Co więcej, niektórzy Obserwatorzy (szczególnie ten, o którym mówimy dzisiaj) dostarczają bardzo przydatne, wstępnie obliczone właściwości, które w innym przypadku zwykliśmy obliczać za pomocą drogich (z punktu widzenia wydajności) metod i właściwości podczas korzystania z regularnych zdarzeń. Aby wyjaśnić tę kwestię, przejdziemy do przykładu w dalszej części artykułu.
Więc jeśli trudno jest komuś odejść od paradygmatu zdarzeń, powiedziałbym, że Obserwatorzy to zdarzenia na sterydach. Inny opis brzmiałby: Obserwatorzy to nowy poziom przybliżenia na szczycie wydarzeń. Ale bez względu na to, którą definicję wolisz, powinno być oczywiste, że Obserwatorzy nie mają na celu zastępowania zdarzeń (przynajmniej jeszcze nie); jest wystarczająco dużo przypadków użycia dla obu i mogą szczęśliwie żyć obok siebie.
Ogólna struktura obserwatora
Ogólna struktura obserwatora (dowolnego z dostępnych w chwili pisania tego tekstu) wygląda podobnie do tej:
/** * Typical Observer's registration */ let observer = new YOUR-TYPE-OF-OBSERVER(function (entries) { // entries: Array of observed elements entries.forEach(entry => { // Here we can do something with each particular entry }); }); // Now we should tell our Observer what to observe observer.observe(WHAT-TO-OBSERVE);
Ponownie zauważ, że entries
są Array
wartości, a nie pojedynczym wpisem.
Oto ogólna struktura: implementacje poszczególnych Observerów różnią się argumentami przekazywanymi do funkcji observe()
i argumentami przekazywanymi do funkcji zwrotnej. Na przykład MutationObserver
powinien również otrzymać obiekt konfiguracyjny, aby dowiedzieć się więcej o tym, jakie zmiany w DOM należy obserwować. PerformanceObserver
nie obserwuje węzłów w DOM, ale zamiast tego ma dedykowany zestaw typów wpisów, które może obserwować.
Tutaj zakończmy „ogólną” część tej dyskusji i zagłębmy się w temat dzisiejszego artykułu — IntersectionObserver
.
Dekonstruowanie obserwatora skrzyżowania
Przede wszystkim ustalmy, czym jest IntersectionObserver
.
Według MDN:
Interfejs API Intersection Observer umożliwia asynchroniczną obserwację zmian w przecięciu elementu docelowego z elementem przodka lub widokiem dokumentu najwyższego poziomu.
Mówiąc najprościej, IntersectionObserver
asynchronicznie obserwuje nakładanie się jednego elementu na inny. Porozmawiajmy o tym, do czego służą te elementy w IntersectionObserver
.
Inicjalizacja obserwatora skrzyżowania
W jednym z poprzednich akapitów widzieliśmy strukturę ogólnego Obserwatora. IntersectionObserver
nieco rozszerza tę strukturę. Przede wszystkim tego typu Observer wymaga konfiguracji z trzema głównymi elementami:
-
root
: jest to element główny używany do obserwacji. Definiuje podstawową „ramkę przechwytywania” dla obserwowalnych elementów. Domyślnieroot
jest widok przeglądarki, ale tak naprawdę może to być dowolny element w DOM (wtedy ustawiaszroot
na coś w rodzajudocument.getElementById('your-element')
). Pamiętaj jednak, że elementy, które chcesz obserwować, muszą w tym przypadku „żyć” w drzewie DOMroot
.
-
rootMargin
: definiuje margines wokół elementuroot
, który rozszerza lub zmniejsza „ramkę przechwytywania”, gdy wymiary elementuroot
nie zapewniają wystarczającej elastyczności. Opcje wartości tej konfiguracji są podobne do wartościmargin
w CSS, na przykładrootMargin: '50px 20px 10px 40px'
(góra, prawy dół, lewy). Wartości mogą być skrócone (jakrootMargin: '50px'
) i mogą być wyrażone wpx
lub%
. DomyślnierootMargin: '0px'
.
-
threshold
: nie zawsze chce reagować natychmiast, gdy obserwowany element przecina granicę „ramki przechwytywania” (zdefiniowanej jako kombinacjaroot
irootMargin
).threshold
określa procent takiego skrzyżowania, na którym powinien zareagować Obserwator. Może być zdefiniowany jako pojedyncza wartość lub jako tablica wartości. Aby lepiej zrozumieć wpływthreshold
(wiem, że czasami może to być mylące), oto kilka przykładów:-
threshold: 0
: Wartość domyślnaIntersectionObserver
powinna reagować, gdy pierwszy lub ostatni piksel obserwowanego elementu przecina jedną z granic „klatki przechwytywania”. Należy pamiętać, żeIntersectionObserver
jest niezależny od kierunku, co oznacza, że zareaguje w obu scenariuszach: a) gdy element wejdzie i b) gdy opuści „ramkę przechwytywania”. -
threshold: 0.5
: Obserwator powinien zostać uruchomiony, gdy 50% obserwowanego elementu przecina „klatkę przechwytywania”; -
threshold: [0, 0.2, 0.5, 1]
: Obserwator powinien zareagować w 4 przypadkach:- Pierwszy piksel obserwowanego elementu wchodzi do „klatki przechwytywania”: element nadal tak naprawdę nie znajduje się w tej klatce lub ostatni piksel obserwowanego elementu opuszcza „ramkę przechwytywania”: element nie znajduje się już w ramce;
- 20% elementu znajduje się w „ramce przechwytywania” (znowu kierunek nie ma znaczenia dla
IntersectionObserver
); - 50% elementu znajduje się w „ramce przechwytywania”;
- 100% elementu znajduje się w „ramce przechwytywania”. Jest to dokładnie przeciwne do
threshold: 0
.
-
Aby poinformować naszego IntersectionObserver
o pożądanej konfiguracji, po prostu przekazujemy nasz obiekt config
do konstruktora naszego Observera wraz z naszą funkcją zwrotną w następujący sposób:
const config = { root: null, // avoiding 'root' or setting it to 'null' sets it to default value: viewport rootMargin: '0px', threshold: 0.5 }; let observer = new IntersectionObserver(function(entries) { … }, config);
Teraz powinniśmy podać IntersectionObserver
rzeczywisty element do obserwowania. Odbywa się to po prostu przez przekazanie elementu do funkcji observe()
:
… const img = document.getElementById('image-to-observe'); observer.observe(image);
Kilka rzeczy do zapamiętania na temat tego obserwowanego elementu:
- Wspomniano o tym wcześniej, ale warto wspomnieć jeszcze raz: W przypadku ustawienia
root
jako elementu w DOM, obserwowany element powinien znajdować się w drzewie DOMroot
. -
IntersectionObserver
może akceptować tylko jeden element do obserwacji na raz i nie obsługuje dostaw partii dla obserwacji. Oznacza to, że jeśli chcesz obserwować kilka elementów (powiedzmy kilka obrazków na stronie), musisz przejrzeć je wszystkie i obserwować każdy z nich z osobna:
… const images = document.querySelectorAll('img'); images.forEach(image => { observer.observe(image); });
- Podczas ładowania strony z zainstalowanym Observerem możesz zauważyć, że wywołanie zwrotne
IntersectionObserver
zostało uruchomione dla wszystkich obserwowanych elementów jednocześnie. Nawet te, które nie pasują do dostarczonej konfiguracji. „Cóż… nie do końca to, czego się spodziewałem”, to zwykła myśl, gdy doświadczam tego po raz pierwszy. Ale nie dajcie się zmylić: nie musi to oznaczać, że obserwowane elementy w jakiś sposób przecinają „ramkę przechwytywania” podczas ładowania strony.
Oznacza to jednak, że wpis dla tego elementu został zainicjowany i jest teraz kontrolowany przez Twój IntersectionObserver
. Może to jednak dodać niepotrzebny szum do funkcji zwrotnej, a Twoim obowiązkiem jest wykrycie, które elementy rzeczywiście przecinają „ramkę przechwytywania”, a których nadal nie musimy uwzględniać. Aby zrozumieć, jak przeprowadzić to wykrywanie, zagłębmy się nieco w anatomię naszej funkcji zwrotnej i przyjrzyjmy się, z czego składają się takie wpisy.
Wywołanie zwrotne obserwatora skrzyżowania
Po pierwsze, funkcja wywołania zwrotnego dla obiektu IntersectionObserver
przyjmuje dwa argumenty, o których będziemy mówić w odwrotnej kolejności, zaczynając od drugiego argumentu. Wraz ze wspomnianą wcześniej Array
obserwowanych wpisów, przecinającą naszą „przechwytującą ramkę”, funkcja wywołania zwrotnego otrzymuje jako drugi argument sam Observer .
Odniesienie do samego obserwatora
new IntersectionObserver(function(entries, SELF) {…});
Uzyskanie odniesienia do samego Observera jest przydatne w wielu scenariuszach, gdy chcesz przestać obserwować jakiś element po wykryciu go przez IntersectionObserver
po raz pierwszy. Tego rodzaju są scenariusze, takie jak leniwe ładowanie obrazów, odroczone pobieranie innych zasobów itp. Gdy chcesz przestać obserwować element, IntersectionObserver
udostępnia metodę unobserve(element-to-stop-observing)
, którą można uruchomić w funkcji zwrotnej po wykonaniu pewnych działań na obserwowanym elemencie (takich jak rzeczywiste leniwe ładowanie obrazu, na przykład ).
Niektóre z tych scenariuszy zostaną omówione w dalszej części artykułu, ale pomijając ten drugi argument, przejdźmy do głównych aktorów tego odtwarzania wywołań zwrotnych.
SkrzyżowanieObserwatorWejście
new IntersectionObserver(function(ENTRIES, self) {…});
entries
, które otrzymujemy w naszej funkcji zwrotnej jako Array
, są specjalnego typu: IntersectionObserverEntry
. Interfejs ten zapewnia nam predefiniowany i wstępnie obliczony zestaw właściwości dotyczących każdego konkretnego obserwowanego elementu. Przyjrzyjmy się tym najciekawszym.
Po pierwsze, wpisy typu IntersectionObserverEntry
zawierają informacje o trzech różnych prostokątach — definiujących współrzędne i granice elementów biorących udział w procesie:
-
rootBounds
: prostokąt dla „ramki przechwytywania” (root
+rootMargin
); -
boundingClientRect
: prostokąt dla samego obserwowanego elementu; -
intersectionRect
: Obszar „ramki przechwytywania” przecinany przez obserwowany element.
Naprawdę fajną rzeczą w tych prostokątach obliczanych dla nas asynchronicznie jest to, że dostarczają nam ważnych informacji związanych z pozycjonowaniem elementu bez wywoływania przez nas getBoundingClientRect()
, offsetTop
, offsetLeft
i innych kosztownych właściwości i metod pozycjonowania, które powodują zaśmiecanie układu. Czysta wygrana za wydajność!
Inną interesującą nas właściwością interfejsu IntersectionObserverEntry
jest isIntersecting
. Jest to wygodna właściwość wskazująca, czy obserwowany element aktualnie przecina „klatkę przechwytywania”, czy nie. Moglibyśmy oczywiście uzyskać te informacje, patrząc na intersectionRect
(jeśli ten prostokąt nie ma wartości 0×0, element przecina „klatkę przechwytywania”), ale wcześniejsze obliczenie jest dla nas całkiem wygodne.
isIntersecting
może służyć do sprawdzenia, czy obserwowany element dopiero wchodzi w „klatkę przechwytywania”, czy już ją opuszcza. Aby się tego dowiedzieć, zapisz wartość tej właściwości jako flagę globalną, a kiedy nowy wpis dla tego elementu dotrze do funkcji wywołania zwrotnego, porównaj nowe isIntersecting
z tą flagą globalną:
- Jeśli był
false
, a teraz jesttrue
, to element wchodzi w „klatkę przechwytywania”; - Jeśli jest odwrotnie i teraz jest
false
, podczas gdy wcześniej byłotrue
, to element opuszcza „klatkę przechwytywania”.
isIntersecting
to dokładnie właściwość, która pomaga nam rozwiązać problem, o którym mówiliśmy wcześniej, tj. oddzielne wpisy dla elementów, które naprawdę przecinają „klatkę przechwytywania” z szumem elementów, które są tylko inicjalizacją wpisu.
let isLeaving = false; let observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { // we are ENTERING the "capturing frame". Set the flag. isLeaving = true; // Do something with entering entry } else if (isLeaving) { // we are EXITING the "capturing frame" isLeaving = false; // Do something with exiting entry } }); }, config);
UWAGA : W Microsoft Edge 15 właściwość isIntersecting
nie została zaimplementowana, zwracając undefined
pomimo pełnego wsparcia dla IntersectionObserver
w przeciwnym razie. Zostało to jednak naprawione w lipcu 2017 i jest dostępne od Edge 16.
Interfejs IntersectionObserverEntry
udostępnia jeszcze jedną wstępnie obliczoną właściwość wygody: intersectionRatio
. Ten parametr może być używany do tych samych celów, co isIntersecting
, ale zapewnia bardziej szczegółową kontrolę, ponieważ jest liczbą zmiennoprzecinkową zamiast wartości logicznej. Wartość intersectionRatio
wskazuje, jaka część obszaru obserwowanego elementu przecina „ramkę przechwytywania” (stosunek obszaru intersectionRect
do obszaru boundingClientRect
). Znowu moglibyśmy wykonać te obliczenia sami, korzystając z informacji z tych prostokątów, ale dobrze jest zrobić to za nas.
target
to jeszcze jedna właściwość interfejsu IntersectionObserverEntry
, do której możesz potrzebować dość często. Ale nie ma tu absolutnie żadnej magii – to tylko oryginalny element, który został przekazany do funkcji observe()
Twojego Observera. Podobnie jak event.target
, do którego przywykłeś podczas pracy z wydarzeniami.
Aby uzyskać pełną listę właściwości interfejsu IntersectionObserverEntry
, sprawdź specyfikację.
Możliwe zastosowania
Zdaję sobie sprawę, że najprawdopodobniej trafiłeś do tego artykułu właśnie z powodu tego rozdziału: kogo obchodzi mechanika, skoro w końcu mamy fragmenty kodu do kopiowania i wklejania? Więc nie będę cię teraz zawracał głowy dalszą dyskusją: wkraczamy w krainę kodu i przykładów. Mam nadzieję, że komentarze zawarte w kodzie wyjaśnią sprawę.
Odroczona funkcjonalność
Przede wszystkim przejrzyjmy przykład ujawniający podstawowe zasady leżące u podstaw idei IntersectionObserver
. Powiedzmy, że masz element, który musi wykonać wiele obliczeń, gdy pojawi się na ekranie. Na przykład reklama powinna rejestrować wyświetlenie tylko wtedy, gdy faktycznie została pokazana użytkownikowi. Ale teraz wyobraźmy sobie, że gdzieś poniżej pierwszego ekranu na Twojej stronie znajduje się automatycznie odtwarzany element karuzeli.
Generalnie prowadzenie karuzeli jest ciężkim zadaniem. Zwykle wiąże się to z licznikami czasu JavaScript, obliczeniami do automatycznego przewijania elementów itp. Wszystkie te zadania ładują główny wątek, a kiedy odbywa się to w trybie autoodtwarzania, trudno nam określić, kiedy nasz wątek główny otrzyma to uderzenie. Kiedy mówimy o nadaniu priorytetu treści na naszym pierwszym ekranie i chcemy jak najszybciej uderzyć w Pierwsze znaczące malowanie i Czas na interaktywność, zablokowany wątek główny staje się wąskim gardłem dla naszej wydajności.
Aby rozwiązać ten problem, możemy odroczyć odtwarzanie takiej karuzeli do czasu, gdy znajdzie się ona w widocznym obszarze przeglądarki. W tym przypadku wykorzystamy naszą wiedzę i przykład dla parametru isIntersecting
interfejsu IntersectionObserverEntry
.
const carousel = document.getElementById('carousel'); let isLeaving = false; let observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { isLeaving = true; entry.target.startCarousel(); } else if (isLeaving) { isLeaving = false; entry.target.stopCarousel(); } }); } observer.observe(carousel);
Tutaj odtwarzamy karuzelę tylko wtedy, gdy dostanie się ona do naszego okienka ekranu. Zwróć uwagę na brak obiektu config
przekazanego do inicjalizacji IntersectionObserver
: oznacza to, że polegamy na domyślnych opcjach konfiguracyjnych. Kiedy karuzela wyjdzie z naszego okienka ekranu, powinniśmy przestać ją odtwarzać, aby nie wydawać zasobów na mniej ważne elementy.
Leniwe ładowanie aktywów
Jest to prawdopodobnie najbardziej oczywisty przypadek użycia IntersectionObserver
: nie chcemy wydawać zasobów na pobranie czegoś, czego użytkownik w tej chwili nie potrzebuje. Daje to ogromne korzyści Twoim użytkownikom: użytkownicy nie będą musieli pobierać, a ich urządzenia mobilne nie będą musiały analizować i kompilować wielu bezużytecznych informacji, których w danej chwili nie potrzebują. Nic dziwnego, że poprawi to również wydajność Twojej aplikacji.
Wcześniej, aby odroczyć pobieranie i przetwarzanie zasobów do momentu, w którym użytkownik może je wyświetlić na ekranie, mieliśmy do czynienia z detektorami zdarzeń na zdarzeniach takich jak scroll
. Problem jest oczywisty: zbyt często wyzwalało to słuchaczy. Musieliśmy więc wpaść na pomysł ograniczenia lub odrzucenia wykonania wywołania zwrotnego. Ale wszystko to spowodowało duży nacisk na nasz główny wątek, blokując go, gdy najbardziej tego potrzebowaliśmy.
Wracając więc do IntersectionObserver
w scenariuszu z leniwym ładowaniem, na co powinniśmy zwracać uwagę? Sprawdźmy prosty przykład leniwego ładowania obrazów.
Spróbuj powoli przewinąć tę stronę do „trzeciego ekranu” i obserwuj okno monitorowania w prawym górnym rogu: poinformuje Cię, ile obrazów zostało do tej pory pobranych.
W rdzeniu znaczników HTML do tego zadania znajduje się prosta sekwencja obrazów:
… <img data-src="https://blah-blah.com/foo.jpg"> …
Jak widać, obrazy powinny być dostarczane bez tagów src
: gdy przeglądarka zobaczy atrybut src
, od razu zacznie pobierać ten obraz, co jest sprzeczne z naszymi intencjami. Dlatego nie powinniśmy umieszczać tego atrybutu w naszych obrazach w HTML, a zamiast tego możemy polegać na jakimś atrybucie data-
, takim jak data-src
tutaj.
Kolejną częścią tego rozwiązania jest oczywiście JavaScript. Skupmy się tutaj na głównych fragmentach:
const images = document.querySelectorAll('[data-src]'); const config = { … }; let observer = new IntersectionObserver(function (entries, self) { entries.forEach(entry => { if (entry.isIntersecting) { … } }); }, config); images.forEach(image => { observer.observe(image); });
Jeśli chodzi o strukturę, nie ma tu nic nowego: omówiliśmy to wszystko już wcześniej:
- Otrzymujemy wszystkie wiadomości z naszymi atrybutami
data-src
; - Ustaw
config
: w tym scenariuszu chcesz rozszerzyć swoją „klatkę przechwytywania”, aby wykrywać elementy nieco poniżej dolnej części widocznego obszaru; - Zarejestruj
IntersectionObserver
z tą konfiguracją; - Iteruj po naszych obrazach i dodaj je wszystkie, aby były obserwowane przez ten
IntersectionObserver
;
Ciekawa część ma miejsce w ramach funkcji zwrotnej wywołanej na wpisach. W grę wchodzą trzy podstawowe kroki.
Przede wszystkim przetwarzamy tylko te elementy, które naprawdę przecinają naszą „klatkę przechwytywania”. Ten fragment powinien być Ci już znany.
entries.forEach(entry => { if (entry.isIntersecting) { … } });
Następnie w jakiś sposób przetwarzamy wpis, konwertując nasz obraz z
data-src
na rzeczywisty<img src="…">
.if (entry.isIntersecting) { preloadImage(entry.target); … }
preloadImage()
to bardzo prosta funkcja, o której nie warto tutaj wspominać. Po prostu przeczytaj źródło.Kolejny i ostatni krok: ponieważ leniwe ładowanie jest czynnością jednorazową i nie musimy pobierać obrazu za każdym razem, gdy element trafia do naszej „klatki przechwytywania”, powinniśmy
unobserve
już przetworzonego obrazu. W ten sam sposób, w jaki powinniśmy to zrobić za pomocąelement.removeEventListener()
dla naszych zwykłych zdarzeń, gdy nie są one już potrzebne, aby zapobiec wyciekom pamięci w naszym kodzie.if (entry.isIntersecting) { preloadImage(entry.target); // Observer has been passed as
self
to our callback self.unobserve(entry.target); }
Notatka. Zamiast unobserve(event.target)
moglibyśmy również wywołać Disconnect disconnect()
: całkowicie odłącza on nasz IntersectionObserver
i nie będzie już obserwował obrazów. Jest to przydatne, jeśli zależy ci tylko na pierwszym trafieniu twojego obserwatora. W naszym przypadku potrzebujemy obserwatora, aby monitorował obrazy, więc nie powinniśmy się jeszcze rozłączać.
Zachęcamy do rozwidlenia przykładu i zabawy z różnymi ustawieniami i opcjami. Jest jednak jedna interesująca rzecz, o której należy wspomnieć , gdy chcesz w szczególności leniwie ładować obrazy. Należy zawsze mieć na uwadze pudełko wygenerowane przez obserwowany element! Jeśli sprawdzisz przykład, zauważysz, że CSS dla obrazów w wierszach 41–47 zawiera rzekomo zbędne style, w tym. min-height: 100px
. Ma to na celu nadanie symbolom zastępczym obrazu ( <img>
bez atrybutu src
) pewnego wymiaru pionowego. Po co?
- Bez wymiarów pionowych wszystkie
<img>
generowałyby pole 0×0; - Ponieważ tag
<img>
domyślnie generuje jakiś rodzaj polainline-block
w wierszu, wszystkie te pola 0×0 zostaną wyrównane obok siebie w tej samej linii; - Oznacza to, że Twój
IntersectionObserver
zarejestrowałby wszystkie (lub, w zależności od szybkości przewijania, prawie wszystkie) obrazy naraz — prawdopodobnie nie do końca to, co chcesz osiągnąć.
Wyróżnienie bieżącej sekcji
Oczywiście IntersectionObserver
to znacznie więcej niż tylko leniwe ładowanie. Oto kolejny przykład zastąpienia zdarzenia scroll
tą technologią. W tym przypadku mamy dość powszechny scenariusz: na stałym pasku nawigacyjnym powinniśmy podświetlić bieżącą sekcję na podstawie pozycji przewijania dokumentu.
Strukturalnie jest podobny do przykładu z leniwym ładowaniem obrazów i ma taką samą strukturę podstawową z następującymi wyjątkami:
- Teraz chcemy obserwować nie obrazy, ale sekcje na stronie;
- Oczywiście mamy też inną funkcję do przetwarzania wpisów w naszym wywołaniu zwrotnym (
intersectionHandler(entry)
). Ale ten nie jest interesujący: wszystko, co robi, to przełączanie klasy CSS.
Interesujący jest tu jednak obiekt config
:
const config = { rootMargin: '-50px 0px -55% 0px' };
Dlaczego nie pytasz o domyślną wartość 0px
dla rootMargin
? Po prostu dlatego, że podświetlenie bieżącej sekcji i leniwe ładowanie obrazu to zupełnie inne cele, które staramy się osiągnąć. Przy leniwym ładowaniu chcemy rozpocząć ładowanie, zanim obraz pojawi się w widoku. Dlatego w tym celu rozszerzyliśmy naszą „klatkę przechwytywania” o 50 pikseli na dole. Wręcz przeciwnie, gdy chcemy podświetlić aktualną sekcję, musimy być pewni, że sekcja jest rzeczywiście widoczna na ekranie. I nie tylko to: musimy być pewni, że użytkownik faktycznie czyta lub zamierza przeczytać dokładnie ten rozdział. Dlatego chcemy, aby sekcja przechodziła nieco ponad połowę rzutni od dołu, zanim będziemy mogli zadeklarować ją jako sekcję aktywną. Ponadto chcemy uwzględnić wysokość paska nawigacyjnego, dlatego usuwamy wysokość paska z „ramki przechwytywania”.
Zwróć też uwagę , że w przypadku podświetlenia bieżącego elementu nawigacyjnego nie chcemy przestać niczego obserwować. Tutaj zawsze powinniśmy mieć kontrolę IntersectionObserver
, dlatego nie znajdziesz tutaj ani disconnect()
, ani unobserve()
.
Streszczenie
IntersectionObserver
to bardzo prosta technologia. Ma całkiem dobre wsparcie w nowoczesnych przeglądarkach i jeśli chcesz go zaimplementować w przeglądarkach, które nadal (lub wcale) go obsługują, oczywiście jest do tego wypełnienie. Ale ogólnie rzecz biorąc, jest to świetna technologia, która pozwala nam robić różne rzeczy związane z wykrywaniem elementów w widoku, jednocześnie pomagając osiągnąć naprawdę dobry wzrost wydajności.
Dlaczego IntersectionObserver jest dla Ciebie dobry?
-
IntersectionObserver
to asynchroniczny nieblokujący interfejs API! -
IntersectionObserver
zastępuje nasze drogie detektory przy zdarzeniachscroll
lubresize
. -
IntersectionObserver
wykonuje za Ciebie wszystkie kosztowne obliczenia, takie jakgetClientBoundingRect()
, abyś nie musiał tego robić. -
IntersectionObserver
podąża za strukturalnym wzorcem innych obserwatorów, więc teoretycznie powinno być łatwe do zrozumienia, jeśli znasz sposób działania innych obserwatorów.
Rzeczy, o których należy pamiętać
Jeśli porównamy możliwości IntersectionObserver ze światem window.addEventListener('scroll')
, skąd to wszystko się wzięło, trudno będzie dostrzec jakiekolwiek wady tego obserwatora. Zwróćmy więc uwagę na kilka rzeczy, o których należy pamiętać:
- Tak,
IntersectionObserver
to asynchroniczny nieblokujący interfejs API. Dobrze wiedzieć! Ale jeszcze ważniejsze jest zrozumienie, że kod, który uruchamiasz w wywołaniach zwrotnych, nie będzie domyślnie uruchamiany asynchronicznie, nawet jeśli sam interfejs API jest asynchroniczny. Więc nadal istnieje szansa na wyeliminowanie wszystkich korzyści, jakie dajeIntersectionObserver
, jeśli obliczenia funkcji zwrotnej spowodują, że główny wątek przestanie odpowiadać. Ale to już inna historia. - Jeśli używasz
IntersectionObserver
do leniwego ładowania zasobów (takich jak na przykład obrazy), uruchom.unobserve(asset)
po załadowaniu zasobu. IntersectionObserver
może wykryć przecięcia tylko dla elementów, które pojawiają się w strukturze formatowania dokumentu. Żeby było jasne: obserwowalne elementy powinny generować pudełko i w jakiś sposób wpływać na układ. Oto tylko kilka przykładów, które pomogą Ci lepiej zrozumieć:- Elementy z
display: none
są wykluczone; -
opacity: 0
lubvisibility:hidden
tworzą pole (nawet jeśli niewidoczne), aby zostały wykryte; - Elementy pozycjonowane bezwzględnie o
width:0px; height:0px
width:0px; height:0px
są w porządku. Though, it has to be noted that absolutely positioned elements fully positioned outside of parent's borders (with negative margins or negativetop
,left
, etc.) and are cut out by parent'soverflow: hidden
won't be detected: their box is out of scope for the formatting structure.
- Elementy z
I know it was a long article, but if you're still around, here are some links for you to get an even better understanding and different perspectives on the Intersection Observer API:
- Intersection Observer API on MDN;
- IntersectionObserver polyfill;
- IntersectionObserver polyfill as
npm
module; - Lazy-Loading Images with IntersectionObserver [video] by amazing Paul Lewis;
- Basic and short (just 01:39), but very informative introduction to IntersectionObserver [video] by Surma.
With this, I would like to make a pause in our discussion to give you an opportunity to play with this technology and realize all of its convenience. So, go play with it. The article is finally over. This time I really mean it.