Mundarija (32)
- Bu darsda
- 1. Nega bu kerak?
- 2. Birinchi kuzatuvchi
- 2.1 Kuzatuvchi naqshi
- 2.2 Uch qadam
- 2.3 «Bahor»: kartalar paydo bo'ladi
- 3. Sozlamalar: threshold, root, rootMargin
- 3.1 threshold — "qancha qismi ko'rinsa?"
- 3.2 root — "nimaga nisbatan?"
- 3.3 rootMargin — "ildizni kengaytirish"
- 4. Kuzatishni to'xtatish
- 5. Lazy loading: rasmlarni kechiktirib yuklash
- 5.1 Avval — HTML'ning o'zi
- 5.2 Qachon IntersectionObserver kerak
- 6. Cheksiz lenta (infinite scroll)
- 6.1 Sentinel g'oyasi
- 6.2 «Bahor» menyusi mashq API'dan
- 7. Callback qachon chaqiriladi
- 8. Ko'p uchraydigan xatolar
- 8.1 Callback'da isIntersecting ni tekshirmaslik
- 8.2 Ish tugagach kuzatishni to'xtatmaslik
- 8.3 Ko'rinib turgan qorovul
- 8.4 rootMargin ishlamayapti — sahifa iframe ichida
- 8.5 root ichida bo'lmagan element
- 9. Mashqlar
- 1-mashq (oson): Fon rasmini kechiktirib yuklash
- 2-mashq (o'rta): Scroll-spy — menyu bo'limlari
- 3-mashq (qiyin): "Haqiqatan ko'rildi" hisobi
- 4-mashq: Amaliy tajriba — vazifalar uchun "Yana yuklash"
- 10. Real ishda
- Xulosa
- Manbalar
JavaScript IntersectionObserver: element ekranga chiqqanini kuzatish
Qisqacha:
IntersectionObserver— "shu element ko'rinadigan maydonga kirdimi yoki chiqdimi?" degan savolga brauzerning o'zi javob beradigan kuzatuvchi. Siznew IntersectionObserver(callback, { threshold, rootMargin })yaratasiz vaobserve(element)bilan unga elementlarni berasiz; brauzer faqat holat o'zgarganda callback'ni chaqiradi.scrolltinglovchisi vagetBoundingClientRect()dan ancha arzon — lazy loading, cheksiz lenta va "ko'rinsa animatsiya" shu bilan qilinadi.
Bu darsda
IntersectionObserveryaratib, elementlar ekranga kirib-chiqishini kuzata olasiz.threshold,rootvarootMarginsozlamalarini tanlay olasiz vaintersectionRationi o'qiy olasiz.unobservevadisconnectbilan kuzatishni to'xtata olasiz.- Lazy loading, cheksiz lenta (infinite scroll) va scroll-spy'ni o'zingiz yoza olasiz.
Oldin bilishingiz kerak: Element o'lchamlari va scroll, Fokus, scroll va resize hodisalari, requestAnimationFrame, fetch asoslari.
1. Nega bu kerak?
«Bahor» menyu sahifasida o'nlab taom kartasi bor. Dizayner Nodira shunday xohlaydi: karta ekranga chiqqanda yumshoq paydo bo'lsin. Rasmlar esa faqat mehmon ularga yetib kelganda yuklansin — telefondagi sekin internetni behuda yemasin.
Element o'lchamlari va scroll darsidagi bilim bilan Sardor shunday yozdi: scroll hodisasida har kartaning getBoundingClientRect() ini o'qiydi va "ekrandami?" deb tekshiradi. Ishladi. Lekin:
scrollbir soniyada o'nlab marta keladi, har safar hamma kartalar tekshiriladi.- Har
getBoundingClientRect()— layout'ni majburlaydi (Ko'p elementni samarali chizish dagi muammo). - Hammasi asosiy thread'da. Telefonda aylantirish "qotadi".
O'tgan darsda, Debounce va throttle da, scroll tinglovchisini throttle bilan kamaytirishni o'rgandik. Bu yordam beradi, lekin tekshiruv baribir bizning zimmamizda qoladi.
Brauzer esa elementlar qayerdaligini allaqachon biladi — u chizish uchun hisoblaydi. Shunchaki undan so'rash kerak: "falon element ko'rinsa, menga xabar ber". Shu xizmat — IntersectionObserver. Fokus, scroll va resize darsida "Element ekranga chiqdimi?" savoli uchun maxsus kuzatuvchi bor degan edik. Mana u.
2. Birinchi kuzatuvchi
2.1 Kuzatuvchi naqshi
Kuzatuvchi (observer) — Dizayn naqshlari darsida ko'rgan g'oya: kimdir sizni "kuzatishga yozib qo'yadi" va hodisa bo'lganda o'zi xabar beradi. Siz har soniyada "bo'ldimi?" deb so'ramaysiz.
Hayotdagi o'xshatish — eshik oldidagi qo'riqchi. Jasur aka har daqiqada derazaga chiqib "mehmon keldimi?" deb qaramaydi. U qo'riqchiga aytadi: "kimdir eshikdan kirsa yoki chiqsa — menga ayt". Qo'riqchi faqat o'zgarish bo'lganda xabar beradi.
Brauzerda bir nechta shunday kuzatuvchi bor. Bugun — ko'rinishni kuzatadigan IntersectionObserver. Keyingi darsda — o'lcham va DOM o'zgarishini kuzatadigan ikki "aka-uka".
Intersection inglizcha "kesishish" degani. Kuzatuvchi ikki to'rtburchakning kesishishini tekshiradi: elementniki va ildiz (root) niki. Ildiz — "ko'rinadigan maydon": sukut bo'yicha brauzer oynasining ko'rinadigan qismi (viewport), yoki siz bergan aylantiriladigan blok.
2.2 Uch qadam
// 1) Kuzatuvchi: o'zgarish bo'lganda nima qilish kerak
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
console.log(entry.target.id, entry.isIntersecting);
}
});
// 2) Kuzatishga yozish
observer.observe(document.getElementById("osh"));
// 3) Kerak bo'lmay qolganda — to'xtatish
// observer.unobserve(element) yoki observer.disconnect()- Callback'ga massiv keladi —
entries("yozuvlar"). Bitta kuzatuvchi ko'p elementni kuzatadi; bir paytda bir nechtasi o'zgarsa, hammasi bitta chaqiruvda keladi. - Har yozuv (
IntersectionObserverEntry) — bitta elementning holati. Eng ko'p ishlatiladiganlari:
| Xususiyat | Nima |
|---|---|
target |
qaysi element |
isIntersecting |
hozir ildiz bilan kesishyaptimi (true/false) |
intersectionRatio |
elementning qancha qismi ko'rinadi: 0 dan 1 gacha |
boundingClientRect |
elementning to'rtburchagi (getBoundingClientRect kabi) |
Muhim nozik joy: observe chaqirilishi bilan callback bir marta darhol keladi — hozirgi holat bilan. Ekranda bo'lmagan element uchun ham: isIntersecting: false. Bu "boshlang'ich hisobot". Shuning uchun callback ichida doim isIntersecting ni tekshiring, "chaqirildi — demak ko'rindi" deb o'ylamang.
2.3 «Bahor»: kartalar paydo bo'ladi
Menyu aylantiriladigan blok ichida. Kartalar boshida shaffof; yarmi ko'ringanda visible klassi qo'shiladi va CSS transition ularni ko'rsatadi:
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
.feed { height: 200px; overflow-y: auto; border: 1px solid #ccd;
border-radius: 0.5rem; padding: 0 0.75rem; }
.card { height: 120px; margin: 12px 0; padding: 0.75rem;
box-sizing: border-box; border-radius: 0.5rem;
background: #eef3ee; opacity: 0; transform: translateY(1rem);
transition: opacity 0.4s, transform 0.4s; }
.card.visible { opacity: 1; transform: none; }
.card h3 { margin: 0 0 0.25rem; font-size: 1.1rem; }
@media (prefers-reduced-motion: reduce) {
.card { transition: none; }
}
</style>
<p>Menyuni aylantiring:</p>
<div class="feed" id="feed">
<article class="card"><h3>Osh</h3>35 000 so'm</article>
<article class="card"><h3>Lag'mon</h3>28 000 so'm</article>
<article class="card"><h3>Manti</h3>30 000 so'm</article>
<article class="card"><h3>Ko'k choy</h3>5 000 so'm</article>
</div>
<script>
const feed = document.getElementById("feed");
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
entry.target.classList.add("visible");
const name = entry.target.querySelector("h3").textContent;
console.log(`Ko'rindi: ${name}`);
observer.unobserve(entry.target); // bir marta yetarli
}
}, { root: feed, threshold: 0.5 });
for (const card of document.querySelectorAll(".card")) {
observer.observe(card);
}
</script>Konsolda:
Ko'rindi: OshBoshida faqat «Osh» ko'rinadi. «Lag'mon» ning yarmidan kami ko'rinadi — threshold: 0.5 hali bajarilmagan. Blokni pastga aylantiring: har karta yarmi ko'ringanda paydo bo'ladi, konsolga Ko'rindi: Lag'mon, keyin Manti, Ko'k choy chiqadi. Yuqoriga qaytsangiz — hech narsa chiqmaydi.
Biz Chrome 154 da ikki xil tekshirdik. Blokni 20 px dan sekin aylantirganda konsolda to'rtala nom chiqdi. Bir sakrashda 400 px ga tushirganda esa Lag'mon chiqmadi: u hech bir tekshirilgan kadrda yarmidan ko'p ko'rinmagan — bir kadrda yarmidan kami, keyingisida umuman yo'q. Kuzatuvchi kadrlardagi holatni ko'radi, oradagi yo'lni emas. Bu haqda "Callback qachon chaqiriladi" bo'limida yana gaplashamiz.
Uch narsaga qarang:
root: feed— ildiz sifatida aylantiriladigan blokni berdik. Berilmasa, ildiz — brauzer oynasi.observer.unobserve(entry.target)— karta bir marta ko'rindi, endi uni kuzatish shart emas. Kuzatuvchining ishi kamayadi.prefers-reduced-motion— "harakatni kamaytirish" sozlamasi yoqilgan foydalanuvchiga kartalar animatsiyasiz chiqadi (prefers-reduced-motionva harakat etikasi darsidagi qoida).
Tekshirib ko'ring: Sahifada 50 ta karta bor. Nechta
IntersectionObserveryaratish kerak?
Javob
Bitta. Bitta kuzatuvchi istalgancha elementni observe qila oladi va o'zgarganlarini bitta entries massivida beradi. Har elementga alohida kuzatuvchi ham ishlaydi, lekin behuda: sozlamalar bir xil bo'lsa, bitta yetarli va arzonroq.
3. Sozlamalar: threshold, root, rootMargin
3.1 threshold — "qancha qismi ko'rinsa?"
threshold ("ostona") — elementning qancha ulushi ko'ringanda callback chaqirilishi. Son yoki sonlar massivi:
0(sukut) — bitta piksel ko'rinishi bilan. Va butunlay yo'qolganda.0.5— yarmi ko'ringanda (va yarmidan kami qolganda).1— to'liq ko'ringanda.[0, 0.25, 0.5, 0.75, 1]— har chorakda.
Callback faqat ostonadan o'tganda keladi — har piksel aylantirishda emas. Bu kuzatuvchining tejamkorligi. Sinab ko'ring: qutini blok ichida sekin aylantiring va ulush qanday o'zgarishini kuzating:
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
.feed { height: 200px; overflow-y: auto; border: 1px solid #ccd;
border-radius: 0.5rem; }
.space { height: 220px; }
#box { height: 150px; margin: 0 1rem; border-radius: 0.5rem;
background: #2e7d32; color: #fff; padding: 0.5rem; }
output { font: 700 1.25rem ui-monospace, monospace; }
</style>
<p>Ko'rinadigan ulush: <output id="ratio">…</output></p>
<div class="feed" id="feed">
<div class="space"></div>
<div id="box">«Bahor» aksiyasi</div>
<div class="space"></div>
</div>
<script>
const out = document.getElementById("ratio");
const observer = new IntersectionObserver((entries) => {
const entry = entries[0];
const ratio = entry.intersectionRatio.toFixed(2);
out.textContent = ratio;
const seen = entry.isIntersecting;
console.log(`isIntersecting: ${seen}, ratio: ${ratio}`);
}, {
root: document.getElementById("feed"),
threshold: [0, 0.25, 0.5, 0.75, 1],
});
observer.observe(document.getElementById("box"));
</script>Konsolda:
isIntersecting: false, ratio: 0.00Bu — "boshlang'ich hisobot": quti hali pastda. Aylantirsangiz 0.00, 0.25 ... 1.00 atrofidagi qiymatlar chiqadi. Aniq 0.25 emas, 0.27 yoki 0.31 bo'lishi mumkin: brauzer holatni kadrda bir marta tekshiradi, shu orada quti ostonadan biroz o'tib ketgan bo'ladi. Tez aylantirsangiz, ba'zi ostonalar umuman "sakrab o'tiladi" — masalan, 0.25 dan to'g'ri 0.75 ga. Shuning uchun "aniq 0.5 bo'ldi" deb emas, "0.5 dan oshdimi" deb tekshiring: entry.intersectionRatio >= 0.5.
3.2 root — "nimaga nisbatan?"
root — ildiz element. Sukut bo'yicha (null) — brauzer oynasining ko'rinadigan qismi. Kuzatilayotgan element ildizning ichida (avlodi) bo'lishi kerak.
Bu darsdagi jonli oynalarda doim root berdik: oynalar dars sahifasiga joylashtirilgan va kichik. Haqiqiy saytda ko'pincha root berilmaydi — butun sahifa aylantiriladi. Aylantiriladigan ichki ro'yxat, chat yoki modal oyna bo'lsa — root shu blok.
3.3 rootMargin — "ildizni kengaytirish"
rootMargin — ildiz to'rtburchagini kengaytiradi yoki toraytiradi. Yozilishi CSS margin kabi: "200px", "0px 0px 300px 0px" (yuqori, o'ng, past, chap), foizlar ham bo'ladi. Faqat px va % — rem qabul qilinmaydi.
- Musbat — oldindan bilish.
rootMargin: "300px"— element ekranga chiqishidan 300 px oldin "ko'rindi" deydi. Lazy loading uchun aynan shu kerak: rasm mehmon yetib kelguncha yuklanib ulgursin. - Manfiy — toraytirish.
rootMargin: "0px 0px -50% 0px"— ildizning faqat yuqori yarmi hisobga olinadi. Scroll-spy'da (2-mashq) shunday qilinadi.
flowchart TB
A["rootMargin: 300px<br/>(kengaygan maydon)"] --> B["Ildiz (root)"]
B --> C{"Element shu maydonga<br/>threshold ulushida kirdimi?"}
C -- "ha / chiqdi" --> D["callback(entries)"]
C -- "o'zgarish yo'q" --> E["jim"]Sxemaga qarang: element ko'rinadigan maydonga kirmasdan oldin, kengaytirilgan chegarani kesib o'tganda callback keladi. O'zgarish bo'lmasa — kuzatuvchi jim.
Tekshirib ko'ring: Rasmlar ekranga chiqishdan 500 px oldin yuklana boshlashi kerak. Qanday sozlama berasiz?
Javob
{ rootMargin: "500px 0px" } (yoki "500px") va threshold: 0. Ildiz yuqoridan va pastdan 500 px ga kengayadi — rasm ko'rinadigan maydonga 500 px qolganda isIntersecting: true bo'ladi. threshold: 0 — bitta piksel kirishi yetarli.
4. Kuzatishni to'xtatish
Uchta metod:
| Metod | Nima qiladi |
|---|---|
observer.unobserve(el) |
bitta elementni kuzatishdan chiqaradi |
observer.disconnect() |
hamma elementlarni — kuzatuvchi butunlay to'xtaydi |
observer.takeRecords() |
hali callback'ga yetmagan yozuvlarni darhol oladi (kam kerak) |
Qoida: ish tugagan elementni darhol chiqaring. "Bir marta paydo bo'lish", "bir marta yuklash" — unobserve. Komponent (sahifa qismi) o'chirilganda — disconnect. Aks holda kuzatuvchi DOM'dan olib tashlangan elementlarni ushlab turadi va xotira sizishi ga olib keladi — 12-qismdagi "detached DOM" holati.
Tekshirib ko'ring: Modal oynada 20 ta rasm lazy yuklanadi. Foydalanuvchi oynani 3 ta rasm yuklangach yopdi va oyna DOM'dan o'chirildi. Kuzatuvchi bilan nima qilish kerak?
Javob
observer.disconnect(). Qolgan 17 ta rasm hali kuzatuvda — kuzatuvchi ularga havolani ushlab turadi va axlat yig'uvchi ularni tozalay olmaydi. disconnect bitta chaqiruvda hammasini chiqaradi. Har biriga unobserve yozish shart emas.
5. Lazy loading: rasmlarni kechiktirib yuklash
5.1 Avval — HTML'ning o'zi
Lazy loading ("dangasa yuklash") — rasmni sahifa ochilganda emas, kerak bo'lganda yuklash. Hozir buning uchun JavaScript shart emas. Rasm unumdorligi darsida ko'rgan atribut yetarli:
<img src="osh.webp" alt="Osh" width="400" height="300" loading="lazy">Brauzer loading="lazy" li rasmni ekranga yaqinlashgandagina yuklaydi. Barcha zamonaviy brauzerlarda ishlaydi. Oddiy rasmlar uchun shu eng yaxshi yo'l.
5.2 Qachon IntersectionObserver kerak
Atribut faqat <img> va <iframe> uchun. Boshqa holatlar uchun kuzatuvchi kerak:
- CSS fon rasmi (
background-image) — atribut yo'q. - Og'ir qism: xarita, video pleyer, izohlar bo'limi — ekranga yaqinlashganda yaratiladi.
- Rasm yuklanishini o'zingiz boshqarishingiz kerak (masalan, avval xira kichik nusxa, keyin aniq rasm).
Naqsh bir xil: haqiqiy manzil data-src atributida turadi, kuzatuvchi uni vaqti kelganda src ga ko'chiradi. Uni 1-mashqda yozasiz.
6. Cheksiz lenta (infinite scroll)
6.1 Sentinel g'oyasi
Cheksiz lenta (infinite scroll) — ro'yxat oxiriga yetganda keyingi sahifa o'zi yuklanadi. Instagram, Telegram kanallari, internet do'konlar shunday. Element o'lchamlari darsida buni scrollTop + clientHeight solishtiruvi bilan qilish mumkinligini va "zamonaviy kodda bu ish IntersectionObserver ga topshiriladi" deganimizni eslang.
Usul: ro'yxat oxiriga bo'sh kichik element qo'yiladi — sentinel ("qorovul"). Uni kuzatamiz. Qorovul ko'rindi — demak oxiriga yetdik, keyingi sahifani yuklaymiz. Yangi elementlar qorovuldan oldin qo'shiladi, u yana pastga suriladi.
6.2 «Bahor» menyusi mashq API'dan
Menyu mashq API dan sahifalab keladi: GET https://ilmhamroh.uz/api/mashq/menyu?sahifa=1&hajm=1 → { elementlar, sahifa, keyingi, jami }. Har sahifada bitta taom, keyingi — keyingi sahifa raqami yoki oxirida null. kechikish=300 sekin internetni taqlid qiladi:
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
.feed { height: 260px; overflow-y: auto; border: 1px solid #ccd;
border-radius: 0.5rem; padding: 0 0.75rem; }
.card { height: 140px; margin: 12px 0; padding: 0.75rem;
box-sizing: border-box; border-radius: 0.5rem;
background: #eef3ee; }
.card h3 { margin: 0 0 0.25rem; font-size: 1.1rem; }
#sentinel { padding: 0.5rem 0; color: #555; }
</style>
<div class="feed" id="feed">
<div id="sentinel">Yuklanmoqda…</div>
</div>
<script>
const API = "https://ilmhamroh.uz/api/mashq/menyu";
const feed = document.getElementById("feed");
const sentinel = document.getElementById("sentinel");
let nextPage = 1;
let isLoading = false;
async function loadPage() {
isLoading = true;
const url = `${API}?sahifa=${nextPage}&hajm=1&kechikish=300`;
const response = await fetch(url);
const data = await response.json();
for (const dish of data.elementlar) {
const card = document.createElement("article");
card.className = "card";
card.innerHTML = `<h3></h3><p></p>`;
card.querySelector("h3").textContent = dish.nom;
card.querySelector("p").textContent = `${dish.narx} so'm`;
sentinel.before(card);
}
console.log(`${data.sahifa}-sahifa yuklandi`);
nextPage = data.keyingi;
isLoading = false;
}
const observer = new IntersectionObserver(async (entries) => {
if (!entries[0].isIntersecting || isLoading) return;
await loadPage();
if (nextPage === null) {
observer.disconnect();
sentinel.textContent = "Menyu tugadi";
console.log("Menyu tugadi");
} else {
// qorovul hali ko'rinib turishi mumkin — qayta tekshiramiz
observer.unobserve(sentinel);
observer.observe(sentinel);
}
}, { root: feed, rootMargin: "0px 0px 40px 0px" });
observer.observe(sentinel);
</script>Konsolda:
1-sahifa yuklandi
2-sahifa yuklandiNima bo'ldi, qadamma-qadam:
- Boshida ro'yxat bo'sh, qorovul ko'rinadi — 1-sahifa yuklandi («Osh»).
- «Osh» kartasi qorovulni taxminan 150 px pastga surdi — u hali ham ko'rinadigan maydonda. Lekin kuzatuvchi faqat o'zgarishda xabar beradi: "ko'rinardi — hali ham ko'rinadi" — o'zgarish emas. Shuning uchun
unobserve+observeqildik: qayta yozilgan element uchun "boshlang'ich hisobot" yana keladi. 2-sahifa yuklandi. - Endi qorovul 300 px dan pastda — blok balandligi 260 px,
rootMarginning 40 px i bilan ham maydondan tashqarida. Kuzatuvchi kutadi.
Blokni pastga aylantiring — «Manti», keyin «Ko'k choy» keladi va Menyu tugadi chiqadi. Biz oxirigacha aylantirib tekshirdik: konsolda to'rt sahifa va Menyu tugadi.
isLoading — himoya: so'rov ketayotganda qorovul yana "ko'rinsa", ikkinchi bir xil so'rov ketmasin. Taom nomini textContent bilan yozdik, innerHTML ga to'g'ridan-to'g'ri emas — serverdan kelgan matn HTML sifatida bajarilmasin (XSS himoyasi, shu qism oxirida chuqur).
Maslahat: Cheksiz lentaning qulaylik tomoni ham bor. Klaviatura bilan yuradigan Bobur footer'ga hech qachon yetolmaydi — u yaqinlashgan sayin yangi kartalar qo'shiladi. Ko'p saytlar shuning uchun «Yana yuklash» tugmasini ham qo'yadi yoki footer'ni lentadan tashqariga chiqaradi. Ekran o'quvchi uchun esa yangi kontent kelganini
aria-livehududi bilan aytish mumkin (Dinamik UI'da qulaylik).
Tekshirib ko'ring: Kodda
unobserve+observeqatorlari bo'lmasa, sahifa ochilganda nechta sahifa yuklanadi?
Javob
Bitta. 1-sahifa yuklangach, qorovul hali ham ko'rinadi — holat o'zgarmagan, callback qayta kelmaydi. Foydalanuvchi blokni biroz aylantirib, qorovulni maydondan chiqarib, qayta kiritmaguncha lenta "qotib" turadi. Bu cheksiz lentaning eng mashhur xatosi.
7. Callback qachon chaqiriladi
Kuzatuvchi asinxron ishlaydi. Brauzer kesishishlarni kadrni chizish jarayonida hisoblaydi — requestAnimationFrame darsidagi kadr sxemasida, layout'dan keyin: elementlar qayerda turgani shu paytda aniq bo'ladi. Callback'lar esa keyinroq, alohida task sifatida keladi.
Bundan uchta amaliy xulosa:
- Callback scroll'ni sekinlashtirmaydi — u aylantirish bilan bir vaqtda emas, keyin keladi.
- Element DOM'ga qo'shilgan zahoti uning holati ma'lum emas:
observedan keyingi qatorisIntersectingni bilmaydi. Natija callback'da keladi. - Juda tez aylantirishda oraliq ostonalar o'tkazib yuborilishi mumkin ("
threshold" bo'limida ko'rdik). Kuzatuvchi "har piksel"ni emas, "har kadrdagi holat"ni ko'radi.
scroll + getBoundingClientRect() bilan solishtiring:
scroll tinglovchisi |
IntersectionObserver |
|
|---|---|---|
| Qachon ishlaydi | har aylantirish qadamida | faqat ostonadan o'tganda |
| Layout | har o'qishda majburlanadi | brauzer o'zi hisoblaydi |
| Kod | o'lcham, solishtirish, debounce | sozlama + callback |
| Iframe ichida | faqat o'z hujjati | ota sahifa oynasini ham hisobga oladi |
Yozuvda yana ikki foydali maydon bor. rootBounds — ildizning to'rtburchagi (ildiz begona sahifadagi oyna bo'lsa — null, sizga uni ko'rsatishmaydi). time — holat qachon aniqlangani, performance.now() o'lchovida. "Element necha ms ko'rindi?" kabi hisoblarda time ishonchliroq: callback biroz kech kelsa ham, u aniqlangan paytni beradi.
Tekshirib ko'ring: Kuzatuvchi callback'ida 200 ms og'ir hisob bor. Bu aylantirishni qotiradimi?
Javob
Ha, lekin boshqacha. Callback aylantirish bilan bir vaqtda emas, keyin keladi. Baribir u asosiy thread'da ishlaydi: 200 ms davomida brauzer na bosishga javob beradi, na JavaScript'ga bog'liq yangilanishni chizadi. Kuzatuvchi faqat "qachon" muammosini hal qiladi — callback ichini yengil saqlang.
8. Ko'p uchraydigan xatolar
8.1 Callback'da isIntersecting ni tekshirmaslik
// ❌ "boshlang'ich hisobot" va chiqib ketish ham "ko'rindi" bo'ladi
const observer = new IntersectionObserver((entries) => {
entries.forEach((e) => e.target.classList.add("visible"));
});observe darhol callback beradi — pastdagi kartalar ham ko'rinmasdan "paydo bo'ladi". Tuzatish: if (!entry.isIntersecting) continue;.
8.2 Ish tugagach kuzatishni to'xtatmaslik
Karta bir marta paydo bo'ldi, rasm bir marta yuklandi — kuzatuvchi esa ishlayveradi. Ro'yxatdan o'chirilgan elementlar xotirada qoladi. Tuzatish: unobserve(entry.target); hammasi tugaganda disconnect().
8.3 Ko'rinib turgan qorovul
Cheksiz lenta bir sahifadan keyin to'xtaydi, chunki qorovul ko'rinishda qolgan va "o'zgarish" yo'q. Tuzatish: yuklashdan keyin unobserve + observe (yoki ro'yxat to'lguncha yuklash sikli).
8.4 rootMargin ishlamayapti — sahifa iframe ichida
Sahifangiz boshqa saytning <iframe> ida ochilgan bo'lsa (masalan, reklama, vidjet yoki shu darsdagi jonli oyna), root berilmagan kuzatuvchi ota sahifa oynasi bilan ishlaydi va rootMargin ni e'tiborsiz qoldiradi. Biz Chrome 154 da sinadik: jonli oyna ichida rootMargin: "500px" hech narsani o'zgartirmadi, root: document bilan esa ishladi. Tuzatish: root ni aniq bering — aylantiriladigan blok yoki hujjatning o'zi: { root: document, rootMargin: "500px" }. root: document — "shu sahifaning o'z ko'rinadigan maydoni", ota sahifaniki emas. Xato xabari chiqmaydi — kuzatuvchi jimgina "kech" ishlaydi.
8.5 root ichida bo'lmagan element
root: feed berdingiz, lekin element feed ning ichida emas. Kuzatuvchi xato bermaydi — element shunchaki hech qachon isIntersecting: true bo'lmaydi. Tuzatish: element ildizning avlodi ekanini tekshiring (feed.contains(el)).
9. Mashqlar
1-mashq (oson): Fon rasmini kechiktirib yuklash
Aylantiriladigan blokda uchta .banner bor, har birida data-bg atributi. Banner ekranga chiqishdan 100 px oldin uning style.backgroundImage iga `url(${banner.dataset.bg})` yozilsin, konsolga Yuklandi: <id> chiqsin va banner kuzatuvdan chiqarilsin. Rasm o'rniga data: SVG ishlatamiz (jonli oyna nisbiy fayllarni ochmaydi).
Ishora: dataset — Atributlar va data-* darsidan; rootMargin — pastga 100 px.
Yechim
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
.feed { height: 200px; overflow-y: auto; border: 1px solid #ccd; }
.banner { height: 150px; margin: 0.5rem; background: #ddd center /
cover no-repeat; border-radius: 0.5rem; }
</style>
<div class="feed" id="feed">
<div class="banner" id="osh" data-bg="data:image/svg+xml,%3Csvg
xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25'
height='100%25' fill='%23e8a33d'/%3E%3C/svg%3E"></div>
<div class="banner" id="manti" data-bg="data:image/svg+xml,%3Csvg
xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25'
height='100%25' fill='%232e7d32'/%3E%3C/svg%3E"></div>
<div class="banner" id="choy" data-bg="data:image/svg+xml,%3Csvg
xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25'
height='100%25' fill='%23c0392b'/%3E%3C/svg%3E"></div>
</div>
<script>
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
const banner = entry.target;
banner.style.backgroundImage = `url("${banner.dataset.bg}")`;
console.log(`Yuklandi: ${banner.id}`);
observer.unobserve(banner);
}
}, {
root: document.getElementById("feed"),
rootMargin: "0px 0px 100px 0px",
});
for (const b of document.querySelectorAll(".banner")) {
observer.observe(b);
}
</script>Konsolda:
Yuklandi: osh
Yuklandi: mantiBoshida «osh» ko'rinadi, «manti» esa ko'rinmasa ham kengaytirilgan maydonda (blok 200 px, manti taxminan 166 px dan boshlanadi) — u ham oldindan yuklandi. «choy» faqat aylantirganda. data-bg qiymati qatorlarga bo'lingan — telefonda o'qish uchun; brauzer URL'dagi qator uzilishlarini e'tiborsiz qoldiradi.
2-mashq (o'rta): Scroll-spy — menyu bo'limlari
Scroll-spy ("aylantirish kuzatuvchisi") — sahifaning qaysi bo'limida turganingizni navigatsiyada belgilash. «Bahor» menyusida uch bo'lim: #taomlar, #salatlar, #ichimliklar. Yuqoridagi navigatsiyada joriy bo'lim havolasiga aria-current="true" qo'yilsin (CSS uni qalin qiladi).
Ishora: "joriy" — ildizning yuqori qismida turgan bo'lim. rootMargin: "0px 0px -70% 0px" ildizning faqat yuqori 30% ini qoldiradi. Havolani nav.querySelector(`a[href="#${id}"]`) bilan toping.
Yechim
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
nav a { margin-right: 1rem; color: #2e7d32; }
nav a[aria-current="true"] { font-weight: 700; color: #111; }
.feed { height: 220px; overflow-y: auto; border: 1px solid #ccd;
padding: 0 0.75rem; margin-top: 0.5rem; }
section { min-height: 260px; }
</style>
<nav id="nav">
<a href="#taomlar">Taomlar</a>
<a href="#salatlar">Salatlar</a>
<a href="#ichimliklar">Ichimliklar</a>
</nav>
<div class="feed" id="feed">
<section id="taomlar"><h3>Taomlar</h3><p>Osh, lag'mon, manti</p>
</section>
<section id="salatlar"><h3>Salatlar</h3><p>Achchiq-chuchuk</p>
</section>
<section id="ichimliklar"><h3>Ichimliklar</h3><p>Ko'k choy</p>
</section>
</div>
<script>
const nav = document.getElementById("nav");
function markCurrent(id) {
for (const link of nav.querySelectorAll("a")) {
link.removeAttribute("aria-current");
}
nav.querySelector(`a[href="#${id}"]`)
.setAttribute("aria-current", "true");
console.log(`Joriy bo'lim: ${id}`);
}
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) markCurrent(entry.target.id);
}
}, {
root: document.getElementById("feed"),
rootMargin: "0px 0px -70% 0px",
});
for (const s of document.querySelectorAll("section")) {
observer.observe(s);
}
</script>Konsolda:
Joriy bo'lim: taomlarAylantirsangiz salatlar, keyin ichimliklar belgilanadi. Ildiz faqat yuqori 30% bo'lgani uchun, bir paytda odatda bitta bo'lim "ko'rinadi". aria-current — ekran o'quvchi Malika uchun ham "joriy" degan ma'lumot. Navigatsiya havolasini bosish esa jonli oynada ishlamaydi — u faqat ko'rsatish uchun.
3-mashq (qiyin): "Haqiqatan ko'rildi" hisobi
Jasur aka aksiya bannerini nechta mehmon haqiqatan ko'rganini bilmoqchi. Banner yarmidan ko'pi kamida 1 soniya ko'rinib tursa — bu "ko'rish" (impression) hisoblanadi. 1 soniyadan oldin chiqib ketsa — hisoblanmaydi. Har banner uchun bir martadan ko'p hisoblanmasin. Ko'rilganda konsolga Ko'rildi: <id> chiqsin.
Ishora: threshold: 0.5. Kirganda setTimeout(..., 1000) qo'ying, taymer identifikatorini Map da element bo'yicha saqlang (Map). Chiqqanda clearTimeout (Taymerlar).
Yechim
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
.feed { height: 200px; overflow-y: auto; border: 1px solid #ccd; }
.banner { height: 150px; margin: 0.5rem; border-radius: 0.5rem;
background: #eef3ee; padding: 0.5rem; }
</style>
<div class="feed" id="feed">
<div class="banner" id="aksiya-osh">Osh — 10% chegirma</div>
<div class="banner" id="aksiya-choy">Choy bepul</div>
</div>
<script>
const timers = new Map();
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
const banner = entry.target;
if (entry.isIntersecting) {
const timerId = setTimeout(() => {
console.log(`Ko'rildi: ${banner.id}`);
observer.unobserve(banner);
timers.delete(banner);
}, 1000);
timers.set(banner, timerId);
} else if (timers.has(banner)) {
clearTimeout(timers.get(banner));
timers.delete(banner);
}
}
}, { root: document.getElementById("feed"), threshold: 0.5 });
for (const b of document.querySelectorAll(".banner")) {
observer.observe(b);
}
</script>Konsolda:
Ko'rildi: aksiya-oshBirinchi banner boshidan ko'rinadi — 1 soniyadan keyin hisoblandi. Ikkinchisini tez aylantirib o'tib ketsangiz — hech narsa chiqmaydi, chunki taymer bekor qilindi. Ustida to'xtasangiz — Ko'rildi: aksiya-choy. Haqiqiy reklama tizimlari ham taxminan shu qoidani ishlatadi: "50% piksel, kamida 1 soniya". Kuzatuvchi iframe ichida ham ota sahifa oynasini hisobga olgani uchun reklama bannerlari aynan shu API bilan o'lchanadi.
4-mashq: Amaliy tajriba — vazifalar uchun "Yana yuklash"
Vazifalar 500 ta bo'lsa, ularning hammasini birdan chizish sekin. Kanon kodga tegmasdan tajriba qiling: kurs/mashqlar/13/03-lenta/index.html — 500 ta soxta vazifa (Vazifa 1 ... Vazifa 500) massivda. Ekranga 20 tadan chizilsin: ro'yxat oxiridagi qorovul ko'ringanda keyingi 20 tasi qo'shilsin. Hammasi chizilganda kuzatuvchi to'xtasin. Live Server bilan oching.
Yechim
<ul id="royxat"></ul>
<p id="qorovul">…</p>
<script>
const tasks = Array.from(
{ length: 500 },
(_, i) => `Vazifa ${i + 1}`,
);
const list = document.getElementById("royxat");
const sentinel = document.getElementById("qorovul");
const PAGE_SIZE = 20;
let shown = 0;
function renderNext() {
const part = tasks.slice(shown, shown + PAGE_SIZE);
list.append(...part.map((text) => {
const li = document.createElement("li");
li.textContent = text;
return li;
}));
shown += part.length;
}
const observer = new IntersectionObserver((entries) => {
if (!entries[0].isIntersecting) return;
renderNext();
if (shown === tasks.length) {
observer.disconnect();
sentinel.textContent = `Hammasi: ${shown} ta`;
return;
}
observer.unobserve(sentinel);
observer.observe(sentinel);
}, { rootMargin: "300px" });
observer.observe(sentinel);
</script>Bu yerda root berilmagan — butun sahifa aylantiriladi, Live Server'dagi oddiy sahifada rootMargin ham ishlaydi. Ochilganda ekranni to'ldirguncha bir necha partiya chiziladi (qorovul qayta kuzatilgani uchun), keyin faqat aylantirganda. DevTools Elements panelida <li> lar soni qanday o'sishini kuzating.
git add 13/03-lenta/index.html
git commit -m "13/03: ro'yxatni partiyalab chizish (IO)"10. Real ishda
- Lazy loading. Rasmlar uchun
loading="lazy"; komponentlar, xaritalar, video pleyerlar, izohlar bo'limi —IntersectionObserver. React, Vue kutubxonalarida "ekranga chiqqanda yuklash" komponentlari ichida aynan shu API (React — 17-qismda). - Cheksiz lenta va pagination. Ijtimoiy tarmoqlar, do'konlar, Telegram Web — qorovul naqshi. Server tomonida sahifalash (
sahifa,keyingi) bilan birga keladi. - Analitika va reklama. "Banner ko'rildimi?", "maqolaning qaysi qismigacha o'qishdi?" — reklamaning "haqiqatan ko'rinish" (viewability) o'lchovi shu API'ga qurilgan (3-mashqdagi qoida).
- Animatsiya. "Ekranga chiqqanda paydo bo'lish" — kuzatuvchi + CSS
transition. Zamonaviy CSS'da buning bir qismini JS'siz ham qilsa bo'ladi — Scroll-driven animatsiyalar darsida ko'rgan edingiz. - Intervyu. "Infinite scroll'ni qanday qilasiz?", "scroll hodisasi va
IntersectionObserverfarqi?" — tez-tez so'raladi.
Xulosa
IntersectionObserverelementning ildiz (sukut bo'yicha — oyna) bilan kesishishini kuzatadi va faqat o'zgarishda callback chaqiradi.observedarhol "boshlang'ich hisobot" beradi — callback'da doimisIntersectingni tekshiring.threshold— qancha ulushda;rootMargin— maydonni kengaytirish (oldindan yuklash) yoki toraytirish (scroll-spy).- Ish tugagach
unobserve, butunlay —disconnect. - Cheksiz lentada qorovul ko'rinib qolsa, qayta
observeqiling. - Oddiy rasm uchun
loading="lazy"yetarli; kuzatuvchi — qolgan hamma narsa uchun.
Keyingi dars: ResizeObserver va MutationObserver — kuzatuvchilar oilasining qolgan ikki a'zosi: element o'lchami va DOM o'zgarishlarini kuzatish.
Manbalar
- W3C: "Intersection Observer" — w3c.github.io/IntersectionObserver
- MDN: "Intersection Observer API",
IntersectionObserverEntry— developer.mozilla.org - MDN: "Lazy loading" (
loadingatributi) — developer.mozilla.org
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!