IlmHamroh
JavaScript Full-stack/13-qism. Brauzer API'lari, performans va xavfsizlik3/32-dars20 daqiqa
Mundarija (32)

JavaScript IntersectionObserver: element ekranga chiqqanini kuzatish

Qisqacha: IntersectionObserver — "shu element ko'rinadigan maydonga kirdimi yoki chiqdimi?" degan savolga brauzerning o'zi javob beradigan kuzatuvchi. Siz new IntersectionObserver(callback, { threshold, rootMargin }) yaratasiz va observe(element) bilan unga elementlarni berasiz; brauzer faqat holat o'zgarganda callback'ni chaqiradi. scroll tinglovchisi va getBoundingClientRect() dan ancha arzon — lazy loading, cheksiz lenta va "ko'rinsa animatsiya" shu bilan qilinadi.

Bu darsda

  • IntersectionObserver yaratib, elementlar ekranga kirib-chiqishini kuzata olasiz.
  • threshold, root va rootMargin sozlamalarini tanlay olasiz va intersectionRatio ni o'qiy olasiz.
  • unobserve va disconnect bilan 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:

  • scroll bir 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

js
// 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:

html
<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:

text
Ko'rindi: Osh

Boshida 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-motion va harakat etikasi darsidagi qoida).

Tekshirib ko'ring: Sahifada 50 ta karta bor. Nechta IntersectionObserver yaratish 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:

html
<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:

text
isIntersecting: false, ratio: 0.00

Bu — "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:

html
<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:

html
<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:

text
1-sahifa yuklandi
2-sahifa yuklandi

Nima bo'ldi, qadamma-qadam:

  1. Boshida ro'yxat bo'sh, qorovul ko'rinadi — 1-sahifa yuklandi («Osh»).
  2. «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 + observe qildik: qayta yozilgan element uchun "boshlang'ich hisobot" yana keladi. 2-sahifa yuklandi.
  3. Endi qorovul 300 px dan pastda — blok balandligi 260 px, rootMargin ning 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-live hududi bilan aytish mumkin (Dinamik UI'da qulaylik).

Tekshirib ko'ring: Kodda unobserve + observe qatorlari 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: observe dan keyingi qator isIntersecting ni 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

js
// ❌ "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
html
<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:

text
Yuklandi: osh
Yuklandi: manti

Boshida «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
html
<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:

text
Joriy bo'lim: taomlar

Aylantirsangiz 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
html
<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:

text
Ko'rildi: aksiya-osh

Birinchi 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
html
<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.

bash
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 IntersectionObserver farqi?" — tez-tez so'raladi.

Xulosa

  • IntersectionObserver elementning ildiz (sukut bo'yicha — oyna) bilan kesishishini kuzatadi va faqat o'zgarishda callback chaqiradi.
  • observe darhol "boshlang'ich hisobot" beradi — callback'da doim isIntersecting ni 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 observe qiling.
  • 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" (loading atributi) — developer.mozilla.org
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
JavaScript IntersectionObserver: element ekranga chiqqanini kuzatish — IlmHamroh